Skip to content

Commit 7a8dcd4

Browse files
authored
Merge pull request #1530 from dhensby/pulls/per-req-timeout
feat: add ability to set per-request requestTimeout
2 parents a10b201 + 3b1645f commit 7a8dcd4

14 files changed

Lines changed: 308 additions & 23 deletions

File tree

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -856,11 +856,22 @@ const config = sql.ConnectionPool.parseConnectionString('Server=localhost,1433;D
856856
## Request
857857

858858
```javascript
859-
const request = new sql.Request(/* [pool or transaction] */)
859+
const request = new sql.Request(/* [pool or transaction], [options] */)
860860
```
861861

862862
If you omit pool/transaction argument, global pool is used instead.
863863

864+
The optional `options` argument allows per-request configuration overrides:
865+
866+
- **requestTimeout** - Override the pool's default request timeout (in ms) for this request only. This applies to queries and stored procedure executions; it does not apply to bulk data transfers (`request.bulk()`), which stream to completion as long as the connection is healthy. If you need to bound a bulk transfer, wrap the call with your own timer and call `request.cancel()`.
867+
868+
```javascript
869+
// Request with a 60-second timeout instead of the pool default
870+
const request = new sql.Request(pool, { requestTimeout: 60000 })
871+
```
872+
873+
**Note:** When using the global pool, you must still pass `undefined` as the first argument to use options: `new sql.Request(undefined, { requestTimeout: 60000 })`.
874+
864875
### Events
865876

866877
- **recordset(columns)** - Dispatched when metadata for new recordset are parsed.
@@ -1226,11 +1237,13 @@ request.cancel()
12261237
**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.
12271238

12281239
```javascript
1229-
const transaction = new sql.Transaction(/* [pool] */)
1240+
const transaction = new sql.Transaction(/* [pool], [options] */)
12301241
```
12311242

12321243
If you omit connection argument, global connection is used instead.
12331244

1245+
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.
1246+
12341247
__Example__
12351248

12361249
```javascript
@@ -1378,11 +1391,13 @@ __Errors__
13781391
**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.
13791392

13801393
```javascript
1381-
const ps = new sql.PreparedStatement(/* [pool] */)
1394+
const ps = new sql.PreparedStatement(/* [pool], [options] */)
13821395
```
13831396

13841397
If you omit the connection argument, the global connection is used instead.
13851398

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

13881403
```javascript

lib/base/connection-pool.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -627,21 +627,23 @@ class ConnectionPool extends EventEmitter {
627627
/**
628628
* Returns new request using this connection.
629629
*
630+
* @param {{ requestTimeout?: number }} [conf] Per-request overrides.
630631
* @return {Request}
631632
*/
632633

633-
request () {
634-
return new shared.driver.Request(this)
634+
request (conf) {
635+
return new shared.driver.Request(this, conf)
635636
}
636637

637638
/**
638639
* Returns new transaction using this connection.
639640
*
641+
* @param {{ requestTimeout?: number }} [conf] Per-transaction overrides, cascaded to child requests.
640642
* @return {Transaction}
641643
*/
642644

643-
transaction () {
644-
return new shared.driver.Transaction(this)
645+
transaction (conf) {
646+
return new shared.driver.Transaction(this, conf)
645647
}
646648

647649
/**

lib/base/prepared-statement.js

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

27-
constructor (parent) {
28+
constructor (parent, overrides = {}) {
2829
super()
2930

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

3944
get config () {
@@ -245,7 +250,7 @@ class PreparedStatement extends EventEmitter {
245250
this._acquiredConnection = connection
246251
this._acquiredConfig = config
247252

248-
const req = new shared.driver.Request(this)
253+
const req = new shared.driver.Request(this, this.overrides)
249254
req._internal = true
250255
req.stream = false
251256
req.output('handle', TYPES.Int)
@@ -328,7 +333,7 @@ class PreparedStatement extends EventEmitter {
328333
*/
329334

330335
_execute (values, callback) {
331-
const req = new shared.driver.Request(this)
336+
const req = new shared.driver.Request(this, this.overrides)
332337
req._internal = true
333338
req.stream = this.stream
334339
req.arrayRowMode = this.arrayRowMode
@@ -409,7 +414,7 @@ class PreparedStatement extends EventEmitter {
409414
return setImmediate(callback, new TransactionError("Can't unprepare the statement. There is a request in progress.", 'EREQINPROG'))
410415
}
411416

412-
const req = new shared.driver.Request(this)
417+
const req = new shared.driver.Request(this, this.overrides)
413418
req._internal = true
414419
req.stream = false
415420
req.input('handle', TYPES.Int, this._handle)

lib/base/request.js

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

33-
constructor (parent) {
34+
constructor (parent, overrides = {}) {
3435
super()
3536

3637
IDS.add(this, 'Request')
@@ -43,6 +44,10 @@ class Request extends EventEmitter {
4344
this.parameters = {}
4445
this.stream = null
4546
this.arrayRowMode = null
47+
this.overrides = {}
48+
if (Number.isFinite(overrides?.requestTimeout) && overrides.requestTimeout >= 0) {
49+
this.overrides.requestTimeout = overrides.requestTimeout
50+
}
4651
}
4752

4853
get paused () {

lib/base/transaction.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ class Transaction extends EventEmitter {
2828
/**
2929
* Create new Transaction.
3030
*
31-
* @param {Connection} [parent] If ommited, global connection is used instead.
31+
* @param {Connection} [parent] If omitted, global connection is used instead.
32+
* @param {{ requestTimeout?: number }} [overrides]
3233
*/
3334

34-
constructor (parent) {
35+
constructor (parent, overrides = {}) {
3536
super()
3637

3738
IDS.add(this, 'Transaction')
@@ -40,6 +41,10 @@ class Transaction extends EventEmitter {
4041
this.parent = parent || globalConnection.pool
4142
this.isolationLevel = Transaction.defaultIsolationLevel
4243
this.name = ''
44+
this.overrides = {}
45+
if (Number.isFinite(overrides?.requestTimeout) && overrides.requestTimeout >= 0) {
46+
this.overrides.requestTimeout = overrides.requestTimeout
47+
}
4348
}
4449

4550
get config () {
@@ -219,11 +224,16 @@ class Transaction extends EventEmitter {
219224
/**
220225
* Returns new request using this transaction.
221226
*
227+
* @param {{ requestTimeout?: number }} [config]
222228
* @return {Request}
223229
*/
224230

225-
request () {
226-
return new shared.driver.Request(this)
231+
request (config) {
232+
const overrides = { ...this.overrides }
233+
if (Number.isFinite(config?.requestTimeout) && config.requestTimeout >= 0) {
234+
overrides.requestTimeout = config.requestTimeout
235+
}
236+
return new shared.driver.Request(this, overrides)
227237
}
228238

229239
/**

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
@@ -365,6 +365,9 @@ class Request extends BaseRequest {
365365

366366
connection.execBulkLoad(bulk, table.rows)
367367
})
368+
if (typeof this.overrides.requestTimeout === 'number') {
369+
req.setTimeout(this.overrides.requestTimeout)
370+
}
368371
this._setCurrentRequest(req)
369372

370373
connection.execSqlBatch(req)
@@ -514,6 +517,10 @@ class Request extends BaseRequest {
514517
}
515518
})
516519

520+
if (typeof this.overrides.requestTimeout === 'number') {
521+
req.setTimeout(this.overrides.requestTimeout)
522+
}
523+
517524
this._setCurrentRequest(req)
518525

519526
req.on('columnMetadata', metadata => {
@@ -874,6 +881,10 @@ class Request extends BaseRequest {
874881
}
875882
})
876883

884+
if (typeof this.overrides.requestTimeout === 'number') {
885+
req.setTimeout(this.overrides.requestTimeout)
886+
}
887+
877888
this._setCurrentRequest(req)
878889

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

lib/tedious/transaction.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const TransactionError = require('../error/transaction-error')
77
const { CHANNELS, publish } = require('../diagnostics')
88

99
class Transaction extends BaseTransaction {
10-
constructor (parent) {
11-
super(parent)
10+
constructor (parent, overrides) {
11+
super(parent, overrides)
1212

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

test/cleanup.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ if exists (select * from sys.procedures where name = '__testInputOutputValue')
2222
if exists (select * from sys.procedures where name = '__testRowsAffected')
2323
exec('drop procedure [dbo].[__testRowsAffected]')
2424

25+
if exists (select * from sys.procedures where name = '__testDelay')
26+
exec('drop procedure [dbo].[__testDelay]')
27+
2528
if exists (select * from sys.types where is_user_defined = 1 and name = 'MSSQLTestType')
2629
exec('drop type [dbo].[MSSQLTestType]')
2730

0 commit comments

Comments
 (0)