Skip to content

fix: resolve parser cache collision with dual typeCast connections #3644

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

MOHIT51196
Copy link

Problem

There was a critical bug in the parser cache system (lib/parsers/parser_cache.js) that caused incorrect type casting when multiple connections used different typeCast configurations simultaneously.

The parser cache generates cache keys based on connection options to reuse parsers for performance. However, the cache key generation for typeCast had a flaw that created ambiguous cache keys.

Issue: When multiple connections had different typeCast boolean values (true vs false), both would generate the same cache key, causing:

  1. Parser Cache Collision: Connection A with typeCast: true would cache a parser
  2. Wrong Parser Reused: Connection B with typeCast: false would find the same cache key and reuse the wrong parser
  3. Incorrect Results: Connection B would get typed results instead of raw Buffers

Real-World Impact

This bug manifested when:

  • Multiple connections existed with different typeCast boolean settings (true vs false)
  • Same queries were executed concurrently or in sequence
  • The same query structure was used across connections

Result: Connections with typeCast: false would incorrectly get typed data instead of raw Buffers, breaking applications that expected raw Buffer data.

Root Cause Analysis

The issue was in the keyFromFields function in lib/parsers/parser_cache.js. The original cache key generation approach created ambiguous keys:

  • Function-based typeCast: Generated unique identifier ✅
  • typeCast: true: Generated generic type identifier ❌
  • typeCast: false: Generated same generic type identifier ❌

This caused cache collisions between typeCast: true and typeCast: false connections because both boolean values were treated identically in the cache key generation process.

Solution

Improved Cache Key Generation: Enhanced the cache key generation to create unique identifiers (if typeCast is boolean) for each distinct typeCast value, eliminating ambiguous cache hits.

Key Improvements

  1. Value-Specific Keys: Each distinct typeCast value (boolean) now generates its own unique cache key and no longer collide in the cache
  2. Backward Compatibility: Function-based typeCast continues to work exactly as before
  3. Deterministic Behavior: Same typeCast value always generates the same cache key

The enhanced approach ensures that:

  • typeCast: true gets its own unique cache key
  • typeCast: false gets a different unique cache key
  • Function typeCast maintains its existing behavior
  • No ambiguity between different boolean values

Test Coverage Added

Created test-typecast-dual-connections.test.cjs to validate the fix and prevent regressions:

The test creates two connections with different typeCast configurations and validates that each connection gets the correct data type format, ensuring no cross-connection parser contamination.

Breaking Changes

None. This fix maintains backward compatibility while resolving the cache collision bug.

Checklist

  • ✅ Fixes critical parser cache collision bug
  • ✅ Maintains backward compatibility
  • ✅ Adds comprehensive test coverage
  • ✅ No performance impact
  • ✅ Handles all typeCast value types properly

@MOHIT51196
Copy link
Author

How to reproduce the error

You can use the following code snippet to reproduce the error

import mysql from 'mysql2/promise';

// Pool 1 - typeCast: true
const pool1 = mysql.createPool({
    host: '<database 1 host>',
    port: 3306,
    user: '<database 1 user>',
    password: '<database 1 password>',
    database: '<database 1 name>',
    connectionLimit: 3,
    typeCast: true
});

// Pool 2 - typeCast: false
const pool2 = mysql.createPool({
    host: '<database 2 host>',
    port: 3306,
    user: '<database 2 user>',
    password: '<database 2 password>',
    database: '<database 2 name>',
    connectionLimit: 5,
    typeCast: false
});

// Query for pool 1
function runQueryUser1() {
    console.log('Creating connection User1');
    return pool1.getConnection()
        .then(connection => {
            const queryString = 'SELECT 1;';
            return connection.query(queryString)
                .then(([rows]) => {
                    console.log(`User1 (typeCast: ${connection.config.typeCast} ) Result:`, JSON.stringify(rows));
                })
                .catch(error => {
                    console.error('User1 Query Error:', error);
                })
                .finally(() => {
                    console.log('Releasing connection');
                    connection.release();
                });
        })
        .catch(error => {
            console.error('User1 Connection Error:', error);
        });
}

// Query for pool 2
function runQueryUser2() {
    console.log('Creating connection User2');
    return pool2.getConnection()
        .then(connection => {
            const queryString = 'SELECT 1;';
            return connection.query(queryString)
                .then(([rows]) => {
                    console.log(`User2 (typeCast: ${connection.config.typeCast} ) Result:`, JSON.stringify(rows));
                })
                .catch(error => {
                    console.error('User2 Query Error:', error);
                })
                .finally(() => {
                    console.log('Releasing connection');
                    connection.release();
                });
        })
        .catch(error => {
            console.error('User2 Connection Error:', error);
        });
}

// Run queries with proper error handling
async function runQueries() {
    await runQueryUser1();
    await runQueryUser2();
    process.exit(0);
}

// Run the queries
runQueries();

@wellwelwel
Copy link
Collaborator

wellwelwel commented Jun 16, 2025

Hi, @MOHIT51196! Thanks for the issue + PR 🙋🏻‍♂️

We have a test to check the parser cache key serialization in test/esm/unit/parsers/cache-key-serialization.test.mjs, where it try the typeCast against true, undefined and function:



With your implementation, it would be useful to pick one of the 6 tests that repeats the value as true and change it to false to test how it works for each variation, then update the assertion expected values.

Your reproduction example is a great case to validate the behavior, but in practice I think it's more practical to use the existing test. What do you think?

@MOHIT51196
Copy link
Author

MOHIT51196 commented Jun 27, 2025

Hi, @MOHIT51196! Thanks for the issue + PR 🙋🏻‍♂️

We have a test to check the parser cache key serialization in test/esm/unit/parsers/cache-key-serialization.test.mjs, where it try the typeCast against true, undefined and function:

With your implementation, it would be useful to pick one of the 6 tests that repeats the value as true and change it to false to test how it works for each variation, then update the assertion expected values.

Your reproduction example is a great case to validate the behavior, but in practice I think it's more practical to use the existing test. What do you think?

Updating the existing test cases will keep them updated with the change, hence its necessary but in my opinion new test file is also necessary to verify the flow and also better for hardening tests.
I have updated existing tests in lib/parsers/parser_cache.js. Please verify @wellwelwel

The parser cache system had a bug where connections with different
typeCast boolean values (true vs false) would share cached parsers,
causing incorrect data type conversion.

Update `parser_cache.js` with following changes :
- Enhanced cache key generation to create unique keys for each typeCast value
- Now typeCast: true and typeCast: false generate distinct cache keys
- Maintains function-based typeCast compatibility
- Ensures proper cache isolation and eliminates ambiguous cache hits

Add test to validate changes in `test-typecast-dual-connections.test.cjs`
with following changes :
- Added test-typecast-dual-connections.test.cjs to validate the fix
- Tests concurrent connections with different typeCast boolean settings
- Prevents future regressions in parser cache logic

Signed-off-by: Mohit Malhotra <[email protected]>
…vior

Update test expectations for `typeCast` field to reflect actual implementation
where `typeCast` returns the value directly instead of the type.
This aligns tests with the `lib/parsers/parser_cache.js` key generation logic.

Signed-off-by: Mohit Malhotra <[email protected]>
@wellwelwel
Copy link
Collaborator

wellwelwel commented Jul 1, 2025

Please verify @wellwelwel

@MOHIT51196, I'll also reply to your private message here, because I believe it applies to the entire community of contributors and users of the project.

My doubt at the moment is whether this problem only applies to true or false values. Otherwise, it would be interesting to have a dedicated Issue for these cases, for example when having multiple typeCast with different functions that will result in the same cache.

  • This PR combines an Issue + PR, but Issues can be closed and reopened if necessary, the same Issue can have multiple PRs assigned to it, as well as being more practical to discuss and control duplicate reports.

Note

Regarding giving priority to issues or reviews, I'd like to point out that our work is entirely voluntary 🤝

About tests

The point of inserting a test that relies on process.on('exit', ...) and process.on('SIGINT', ...), then forcing a process.exit(0) in the end of each process state, leads to concerns about the test effectiveness.

On the other hand, the current tests seem to cover the scenarios adequately: the significant change is that instead of boolean, the cache will be generated with the boolean value itself (true or false), when used.

  • This reinforces the idea of keeping and adapting the current tests, changing one of the repeated boolean cases as true to false, ensuring that the expected value of the cache key is different for each Boolean state.

About the changes

A particular concern would be the flexibility that the current change brings by allowing any value for typeCast in the cache when it's not a function, but this can be easily addressed, for example:

typeof options.typeCast === 'boolean' ? options.typeCast : typeof options.typeCast
  • This would also help prevent undefined from being assigned as null due to JSON.stringify.

Please, feel free to share your thoughts and ideas 🙋🏻‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants