Skip to content

Commit 4a32717

Browse files
dhensbyCopilot
andcommitted
feat: add socket-level pool validation mode
Add a lightweight 'socket' validation mode for validateConnection config. Instead of running SELECT 1 for every connection checkout (expensive at scale), users can set validateConnection: 'socket' to check the tedious connection state and socket health using synchronous property lookups. Supported values for validateConnection: - true (default): SQL validation via SELECT 1 - 'socket': lightweight state + socket check - false: skip validation entirely This addresses the concern raised in #1834 about SELECT 1 generating millions of unnecessary queries in high-throughput environments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fa5746d commit 4a32717

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

0 commit comments

Comments
 (0)