Skip to content

Commit 1b815b1

Browse files
authored
Merge pull request #1839 from dhensby/feat/socket-level-validation
feat: add socket-level pool validation mode
2 parents e15bc55 + 4a32717 commit 1b815b1

3 files changed

Lines changed: 159 additions & 9 deletions

File tree

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ const config = {
133133
### Connections
134134

135135
* [Pool Management](#pool-management)
136+
* [Connection Validation](#connection-validation)
136137
* [ConnectionPool](#connections-1)
137138
* [connect](#connect-callback)
138139
* [close](#close)
@@ -574,6 +575,30 @@ sql.query('SELECT * FROM [example]').then((result) => {
574575
})
575576
```
576577

578+
### Connection Validation
579+
580+
When a connection is acquired from the pool, it can be validated to ensure it is still usable. This is controlled by the `validateConnection` config option.
581+
582+
```javascript
583+
const config = {
584+
server: 'localhost',
585+
// ...
586+
validateConnection: true // default
587+
}
588+
```
589+
590+
The following values are supported:
591+
592+
| Value | Description |
593+
|---|---|
594+
| `true` (default) | Executes `SELECT 1` against the connection before handing it to the caller. This is the most thorough check — it verifies end-to-end connectivity — but adds a round-trip query for every pool acquisition. |
595+
| `'socket'` | Performs a lightweight, synchronous check of the underlying connection state and TCP socket health. No SQL query is executed. This is significantly cheaper at scale and catches most failure modes (closed connections, destroyed sockets, wrong protocol state), but will not detect issues like server-side session invalidation. **Tedious driver only** — with msnodesqlv8, this value falls back to `SELECT 1` behaviour because native ODBC connections do not expose socket-level properties. |
596+
| `false` | Disables validation entirely. The connection is assumed to be healthy if it has not been flagged as closed or errored. Use this only if your application already handles stale connection errors gracefully. |
597+
598+
#### When to use `'socket'` mode
599+
600+
If your application maintains a large connection pool and you see high volumes of `SELECT 1` queries in your SQL Server monitoring, switching to `'socket'` mode can dramatically reduce overhead. TCP keepalive (enabled by default in tedious at 30-second intervals) will independently detect and close dead connections over time, so the socket-level check provides a good balance between reliability and performance.
601+
577602
## Configuration
578603

579604
The following is an example configuration object:
@@ -608,6 +633,7 @@ const config = {
608633
- **pool.min** - The minimum of connections there can be in the pool (default: `0`).
609634
- **pool.idleTimeoutMillis** - The Number of milliseconds before closing an unused connection (default: `30000`).
610635
- **arrayRowMode** - Return row results as a an array instead of a keyed object. Also adds `columns` array. (default: `false`) See [Handling Duplicate Column Names](#handling-duplicate-column-names)
636+
- **validateConnection** - Controls how connections are validated when acquired from the pool. See [Connection Validation](#connection-validation) for details. (default: `true`)
611637

612638
Complete list of pool options can be found [here](https://github.com/vincit/tarn.js/#usage).
613639

lib/tedious/connection-pool.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,36 @@ class ConnectionPool extends BaseConnectionPool {
117117
}
118118

119119
_poolValidate (tedious) {
120-
if (tedious && !tedious.closed && !tedious.hasError) {
121-
return !this.config.validateConnection || new shared.Promise((resolve) => {
122-
const req = new tds.Request('SELECT 1;', (err) => {
123-
resolve(!err)
124-
})
125-
tedious.execSql(req)
126-
})
120+
if (!tedious || tedious.closed || tedious.hasError) {
121+
return false
122+
}
123+
124+
const mode = this.config.validateConnection
125+
126+
if (!mode) {
127+
return true
128+
}
129+
130+
// Socket-level validation: check connection state and socket health
131+
// without executing a SQL query. Much cheaper than SELECT 1 at scale.
132+
if (mode === 'socket') {
133+
if (tedious.state !== tedious.STATE.LOGGED_IN) {
134+
return false
135+
}
136+
if (!tedious.socket || tedious.socket.destroyed || !tedious.socket.writable) {
137+
return false
138+
}
139+
return true
127140
}
128-
return false
141+
142+
// SQL-level validation (default): execute SELECT 1 to verify the
143+
// connection is fully functional end-to-end.
144+
return new shared.Promise((resolve) => {
145+
const req = new tds.Request('SELECT 1;', (err) => {
146+
resolve(!err)
147+
})
148+
tedious.execSql(req)
149+
})
129150
}
130151

131152
_poolDestroy (tedious) {

test/common/unit.js

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
/* globals describe, it, afterEach */
3+
/* globals describe, it, before, afterEach */
44

55
const sql = require('../../')
66
const assert = require('node:assert')
@@ -863,4 +863,107 @@ describe('connection string auth - tedious', () => {
863863
assert.strictEqual(result, 'UDT_StringArray readonly')
864864
})
865865
})
866+
867+
describe('_poolValidate', () => {
868+
// Reset Promise in case earlier tests replaced it (e.g. FakePromise)
869+
before(() => { sql.Promise = Promise })
870+
871+
function createMockConnection (overrides = {}) {
872+
return {
873+
closed: false,
874+
hasError: false,
875+
STATE: { LOGGED_IN: 'LoggedIn' },
876+
state: { name: 'LoggedIn' },
877+
socket: { destroyed: false, writable: true },
878+
...overrides
879+
}
880+
}
881+
882+
function createPool (validateConnection) {
883+
return new ConnectionPool({ validateConnection })
884+
}
885+
886+
it('returns false for null connection', () => {
887+
const pool = createPool(true)
888+
assert.strictEqual(pool._poolValidate(null), false)
889+
})
890+
891+
it('returns false for closed connection', () => {
892+
const pool = createPool(true)
893+
assert.strictEqual(pool._poolValidate(createMockConnection({ closed: true })), false)
894+
})
895+
896+
it('returns false for errored connection', () => {
897+
const pool = createPool(true)
898+
assert.strictEqual(pool._poolValidate(createMockConnection({ hasError: true })), false)
899+
})
900+
901+
it('returns true without validation when validateConnection is false', () => {
902+
const pool = createPool(false)
903+
assert.strictEqual(pool._poolValidate(createMockConnection()), true)
904+
})
905+
906+
it('socket mode: returns true for healthy connection', () => {
907+
const pool = createPool('socket')
908+
const conn = createMockConnection()
909+
conn.STATE.LOGGED_IN = conn.state
910+
assert.strictEqual(pool._poolValidate(conn), true)
911+
})
912+
913+
it('socket mode: returns false when state is not LOGGED_IN', () => {
914+
const pool = createPool('socket')
915+
const conn = createMockConnection()
916+
conn.state = { name: 'SentLogin7WithStandardLogin' }
917+
assert.strictEqual(pool._poolValidate(conn), false)
918+
})
919+
920+
it('socket mode: returns false when socket is destroyed', () => {
921+
const pool = createPool('socket')
922+
const conn = createMockConnection()
923+
conn.STATE.LOGGED_IN = conn.state
924+
conn.socket = { destroyed: true, writable: false }
925+
assert.strictEqual(pool._poolValidate(conn), false)
926+
})
927+
928+
it('socket mode: returns false when socket is not writable', () => {
929+
const pool = createPool('socket')
930+
const conn = createMockConnection()
931+
conn.STATE.LOGGED_IN = conn.state
932+
conn.socket = { destroyed: false, writable: false }
933+
assert.strictEqual(pool._poolValidate(conn), false)
934+
})
935+
936+
it('socket mode: returns false when socket is null', () => {
937+
const pool = createPool('socket')
938+
const conn = createMockConnection()
939+
conn.STATE.LOGGED_IN = conn.state
940+
conn.socket = null
941+
assert.strictEqual(pool._poolValidate(conn), false)
942+
})
943+
944+
it('query mode: returns a promise (SELECT 1)', () => {
945+
const pool = createPool(true)
946+
const conn = createMockConnection()
947+
conn.execSql = (req) => {
948+
req.callback(null)
949+
}
950+
const result = pool._poolValidate(conn)
951+
assert.ok(result instanceof Promise || (result && typeof result.then === 'function'))
952+
return Promise.resolve(result).then(valid => {
953+
assert.strictEqual(valid, true)
954+
})
955+
})
956+
957+
it('query mode: resolves false on error', () => {
958+
const pool = createPool(true)
959+
const conn = createMockConnection()
960+
conn.execSql = (req) => {
961+
req.callback(new Error('connection lost'))
962+
}
963+
const result = pool._poolValidate(conn)
964+
return Promise.resolve(result).then(valid => {
965+
assert.strictEqual(valid, false)
966+
})
967+
})
968+
})
866969
})

0 commit comments

Comments
 (0)