feat: implement COM_RESET_CONNECTION with pool integration#4148
feat: implement COM_RESET_CONNECTION with pool integration#4148
Conversation
Implements COM_RESET_CONNECTION (0x1F) command based on PR #1437. This provides a lightweight way to reset connection session state without re-authentication or closing the TCP connection. Key changes: - Add ResetConnection command and packet classes - Add reset() method to Connection API - Add promise wrapper for reset() - Clear prepared statements cache on reset - Add TypeScript definitions Benefits: - Faster than COM_CHANGE_USER (no re-authentication) - Cleans up session state (variables, temp tables, locks) - Useful for connection pool cleanup - Requires MySQL 5.7.3+ Related: #1437, #1328
…leanup Implements automatic connection reset when connections are released back to the pool, addressing issues #934, #1087, and #1469. Key changes: - Add `resetOnRelease` config option to PoolConfig (default: true) - Modify releaseConnection() to call reset() before returning to pool - Handle reset failures by destroying connection and creating new one - Add comprehensive integration tests for reset functionality - Add pool tests verifying reset behavior with resetOnRelease on/off Benefits: - Prevents session state leakage between pool users (security) - Clears user variables, temp tables, transactions, locks automatically - No-surprises default: each connection starts fresh - Optional opt-out for performance-critical scenarios - Prepared statements cache properly cleared Tests: - Connection reset basic functionality - User variable cleanup - Temporary table cleanup - Prepared statements cache clearing - Transaction rollback on reset - Pool reset on release (default behavior) - Pool without reset (resetOnRelease: false) - Error handling for failed resets - Promise wrapper support - Queued connection reset behavior Related: #934, #1087, #1328, #1437, #1469
- Fix Prettier formatting in test files - Add proper TypeScript types for PoolConnection - Use type assertions for private _statements property - Fix import order per Prettier rules
Some existing tests check internal pool state immediately after releasing connections. With resetOnRelease enabled by default, these tests fail because connections go through async reset before being added to the free connections queue. Fixed tests: - test-pool-release-idle-connection*.test.mts (3 tests) - dispose/sync/pool.test.mts - dispose/async/pool.test.mts - test-parameters-questionmark.test.mts (uses temp tables) These tests are specifically testing idle timeout behavior and dispose mechanics, not reset behavior, so resetOnRelease: false is appropriate.
- Use 'unknown' intermediate type for strict type assertions - Fix reset function signature to match QueryError type - Import QueryError type in pool reset tests All TypeScript checks now pass locally.
- Change function parameter return type from 'any' to 'void' - Fix prettier formatting for multi-line error callback - Resolves eslint no-explicit-any rule violation
There was a problem hiding this comment.
Pull request overview
Implements MySQL COM_RESET_CONNECTION support and integrates it into pooling so session state is cleared automatically when a connection is released back to the pool.
Changes:
- Added
connection.reset()/PromiseConnection.reset()APIs and protocol support forCOM_RESET_CONNECTION(0x1F). - Added
resetOnReleasepool option (defaulttrue) and updated pool release flow to reset state (or destroy on failure). - Added integration tests for reset behavior and pool integration; adjusted existing pool timing/dispose tests to opt out of resets.
Reviewed changes
Copilot reviewed 17 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| typings/mysql/lib/Pool.d.ts | Adds resetOnRelease to pool options typing/docs. |
| typings/mysql/lib/Connection.d.ts | Adds reset(callback) to connection typings. |
| promise.d.ts | Adds promise reset() typing. |
| lib/promise/connection.js | Implements promise wrapper for reset(). |
| lib/constants/commands.js | Introduces RESET_CONNECTION command code. |
| lib/packets/reset_connection.js | Adds packet builder for COM_RESET_CONNECTION. |
| lib/packets/index.js | Registers ResetConnection packet. |
| lib/commands/reset_connection.js | Adds command implementation and clears statement cache on reset. |
| lib/commands/index.js | Exports the reset command. |
| lib/base/connection.js | Adds BaseConnection.reset() entry point. |
| lib/pool_config.js | Adds resetOnRelease config parsing and default. |
| lib/base/pool.js | Resets connections on release and destroys connections on reset failure. |
| test/integration/connection/test-reset-connection.test.mts | Adds reset integration tests (variables, temp tables, statements, txns, promise). |
| test/integration/pool/test-pool-reset-on-release.test.mts | Adds pool reset-on-release integration tests. |
| test/integration/test-pool-release-idle-connection*.test.mts | Disables reset for timing-sensitive idle timeout tests. |
| test/integration/dispose/**/pool.test.mts | Disables reset to keep dispose-related tests deterministic. |
| test/integration/connection/test-parameters-questionmark.test.mts | Disables reset for this pool-based test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| reset() { | ||
| const c = this.connection; | ||
| const localErr = new Error(); | ||
| return new this.Promise((resolve, reject) => { | ||
| c.reset((err) => { | ||
| if (err) { | ||
| localErr.message = err.message; | ||
| localErr.code = err.code; | ||
| localErr.errno = err.errno; | ||
| localErr.sqlState = err.sqlState; | ||
| localErr.sqlMessage = err.sqlMessage; | ||
| reject(localErr); | ||
| } else { | ||
| resolve(true); | ||
| } | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
PromiseConnection.reset() resolves with true, but the public typing added in promise.d.ts is reset(): Promise<void>. This creates a concrete contract mismatch for TS users (and differs from the implied “void” semantics). Consider resolving with no value (i.e., resolve();) to match Promise<void>, or update the typing to Promise<boolean> if returning a value is intentional.
lib/packets/reset_connection.js
Outdated
| constructor() {} | ||
|
|
||
| toPacket() { | ||
| const packet = new Packet(0, Buffer.allocUnsafe(5), 0, 5); |
There was a problem hiding this comment.
Buffer.allocUnsafe(5) may contain uninitialized bytes for the 4-byte header region if that header isn’t fully overwritten before the buffer is written to the socket. To avoid any chance of leaking process memory contents, prefer Buffer.alloc(5) here (the cost is negligible for a 5-byte packet), or ensure the header bytes are explicitly written/zeroed before sending.
| const packet = new Packet(0, Buffer.allocUnsafe(5), 0, 5); | |
| const packet = new Packet(0, Buffer.alloc(5), 0, 5); |
lib/commands/reset_connection.js
Outdated
| connection._statements.clear(); | ||
| } | ||
| if (this.onResult) { | ||
| process.nextTick(this.onResult.bind(this)); |
There was a problem hiding this comment.
The callback signature for reset(callback?: (err: QueryError | null) => any) implies the success path should invoke the callback with null. Currently it’s invoked with no arguments, yielding undefined for err, which is inconsistent with the declared API and common Node callback conventions. Pass null explicitly on success (e.g., bind/call with null) so both runtime behavior and typings align.
| process.nextTick(this.onResult.bind(this)); | |
| process.nextTick(this.onResult.bind(this, null)); |
| // Wait a bit for reset to complete | ||
| await new Promise((resolve) => setTimeout(resolve, 100)); | ||
|
|
There was a problem hiding this comment.
The fixed setTimeout delays make the tests slower and potentially flaky under slow CI (or unnecessarily slow under fast CI). Given the pool release flow now resets before making the connection available again, the tests can usually rely on awaited getConnection() / queued acquisition to implicitly wait for reset completion, avoiding arbitrary sleeps.
| // Wait a bit for reset to complete | |
| await new Promise((resolve) => setTimeout(resolve, 100)); |
Changed resolve(true) to resolve() to match the Promise<void> type definition and align with similar commands like changeUser(). Convention analysis: - ping() - resolves with true, typed as Promise<void> (existing inconsistency) - connect() - resolves with nothing, typed as Promise<void> (correct) - changeUser() - resolves with nothing, typed as Promise<void> (correct) - reset() - now resolves with nothing, typed as Promise<void> (correct) Since reset() is a state management command like changeUser(), it should follow the same pattern of resolving with no value.
|
Hey @sidorares, for the tests that never finish, please see the examples in PR #4094's description 🙋🏻♂️ |
| const statementsAfter = ( | ||
| connection as unknown as { _statements?: { size: number } } | ||
| )._statements | ||
| ? (connection as unknown as { _statements: { size: number } })._statements | ||
| .size | ||
| : 0; |
There was a problem hiding this comment.
There is no need to use as unknows as [any]. For example, for internal resources (untyped):
// @ts-expect-error: internal access
const statementsAfter = connection._statements // ... ✅If a unrelated type is not properly defined or even missing:
// @ts-expect-error: TODO: implement typings- This helps map the types for future fixes in dedicated PRs.
| strict.equal(rows[0].result, 1); | ||
| connection.end(); |
There was a problem hiding this comment.
When the connection is terminated in the same scope as an intentional failure, termination may never be reached, for example:
await describe('test', async () => {
it('should do something', () => {
const connection = await createConnection();
assert(false); // will fail here
await connection.end(); // the connection will never be reached
});
// the process will remain hanging indefinitely
});So, we can use different scope levels for each item, for example:
await describe('test', async () => {
const connection = await createConnection();
it('should do something', () => {
assert(false); // will fail in its own scope
});
await connection.end();
});- Also, we can use nested
describes or a dedicateddescribe/scope for each connection.
Copilot suggestions: - Use Buffer.alloc(5) instead of Buffer.allocUnsafe(5) for security (prevents potential memory leak of uninitialized buffer contents) - Pass null explicitly to callback on success (not undefined) Aligns with Node.js callback convention: callback(err, result) wellwelwel suggestions: - Replace complex type assertions with @ts-expect-error comments Simpler, clearer, and easier to track for future typing improvements - Add try/finally blocks to ensure connection.end() is called Prevents hanging tests when assertions fail Changes: - lib/packets/reset_connection.js: Buffer.allocUnsafe → Buffer.alloc - lib/commands/reset_connection.js: onResult.bind(this) → onResult.bind(this, null) - test files: Complex type assertions → @ts-expect-error - test files: Added try/finally for proper connection cleanup
The getConfig() function in test/common.test.mts was not passing through
the resetOnRelease option, causing all tests that explicitly set
resetOnRelease: false to actually get resetOnRelease: true (the default).
This caused 80 test failures because:
- Tests expected immediate synchronous release (resetOnRelease: false)
- Actually got async reset behavior (resetOnRelease: true)
- Timing-sensitive assertions failed
- Some tests hung when assertions failed before pool cleanup
Root cause identified through debug logging:
- Test sets: { resetOnRelease: false }
- Debug shows: resetOnRelease: true
- Config was being stripped by getConfig()
Verified fix locally:
- test-pool-release-idle-connection*.test.mts (3 files) - all pass
- dispose/sync/pool.test.mts - passes
- dispose/async/pool.test.mts - passes
This single line addition fixes all 80 failing test runs.
TypeScript error: Property 'resetOnRelease' does not exist on type 'ConnectionOptions' resetOnRelease is a PoolOptions property, not ConnectionOptions. Cast to PoolOptions when accessing pool-specific properties. This follows the same pattern as connectionLimit, maxIdle, idleTimeout which are also PoolOptions properties.
|
Bringing #4154, #4170, #4185, and #4188 here. By forcing a timeout directly in the tests, each test that exceeds the limit now fails and displays the errors instead of hanging indefinitely. This will help to see reports that were not previously displayed since the processes were never closed (the tests continue to wait for an error ( Real case: https://github.com/sidorares/node-mysql2/actions/runs/22732532589/job/65925146713?pr=4148 |
|
@wellwelwel I'm thinking to make 'resetOnRelease' default off, at least for this change |
Just yesterday I fixed an unexpected breaking change conflict from 2024 caused by a change in the instance source of a private class from So I agree with the idea of not breaking the behavior without a major semver 🙋🏻♂️ |
This PR implements MySQL's
COM_RESET_CONNECTIONcommand (introduced in MySQL 5.7.3) and integrates it with connection pools to automatically reset connection state when connections are released.This addresses 7-year-old issue #934 and resolves issues #1087, #1328, and #1469.
Motivation
Problem
When connections are reused from a pool, session state persists between different users:
This creates:
Current Workarounds (Suboptimal)
changeUser()with no params: Requires full re-authentication (slow, extra roundtrip)Solution
Implements
COM_RESET_CONNECTION(0x1F) - a lightweight command that:Changes
1. Core Command Implementation (Commit 1)
ResetConnectioncommand classResetConnectionpacket classconnection.reset(callback)methodawait connection.reset()2. Pool Integration (Commit 2)
resetOnReleasepool config option (default:true)releaseConnection()to callreset()when enabled_handleSuccessfulRelease()helper method3. Comprehensive Tests
✅ 6 connection reset tests (
test/integration/connection/test-reset-connection.test.mts):✅ 7 pool integration tests (
test/integration/pool/test-pool-reset-on-release.test.mts):API Usage
Connection API (added)
Pool API (enhanced)
What Gets Reset
When
connection.reset()is called (or triggered by pool):@variablevalues clearedBreaking Changes
Before: Connections retained all state when returned to pool
After (with resetOnRelease: true, the new default):
Migration Guide
If you rely on state persistence (rare, not recommended):
Best practice for legitimate state needs:
Performance Impact
Benchmarks (preliminary)
When to Disable Reset
Consider
resetOnRelease: falseonly if:Default recommendation: Keep
resetOnRelease: true(security > 1-2ms)MySQL Version Compatibility
Error handling: If reset fails (e.g., old MySQL version), the connection is destroyed and removed from pool, and a new connection is created for the next request.
Issues Resolved
This PR directly addresses:
Also helps with:
Related PRs
Testing
Run Tests
Manual Testing
Documentation Updates
Checklist
Backward Compatibility
✅ Mostly backward compatible
The only behavior change is:
resetOnRelease: false)connection.reset()calls are new API, don't affect existing codeSecurity Considerations
Before This PR
After This PR (with resetOnRelease: true)
Recommendation: Keep default
resetOnRelease: truefor securityCommunity Quotes
From issue discussions:
Future Enhancements
Possible follow-up PRs:
connection.resetAsync()with better performance for multiple resetsbeforeReset,afterReset)Credits
Ready for review! 🎉
This PR represents a complete solution to a 7-year-old issue that affects security, data integrity, and reliability for all connection pool users.