node-mysql2 icon indicating copy to clipboard operation
node-mysql2 copied to clipboard

Concurrent call to mysql breaks after some interaction

Open calebeaires opened this issue 8 years ago • 15 comments

I have created an singleton connection to use the same connection_id() so the rest app can make use of TEMPORARY TABLES. After some concurrent calls, the connection is broken with this error:

node_modules/mysql2/lib/connection.js:127
connection.threadId = handshakeCommand.handshake.connectionId;
TypeError: Cannot read property 'connectionId' of null
at ClientHandshake.<anonymous>

He is the code


import { Me } from '../shared/Me';
import * as knex from 'knex';

export class BucketDb {

    private static _connection: { [sessionId: string]: knex } = {};

    public static connect(connectionData) {

        if (!this._connection || (this._connection && !this._connection[Me.profile().sessionId])) {
            this._connection[Me.profile().sessionId] = knex({
                client: 'mysql2',
                connection: connectionData,
                pool: {
                    min: 0, max: 1
                }
            });
        }

        // this._connection[Me.profile().sessionId].raw('select connection_id()').then((r) => {
        //     console.log(Me.profile().sessionId, r[0][0]);
        // });

        return this._connection[Me.profile().sessionId];

    }

}

calebeaires avatar Jul 09 '17 15:07 calebeaires

I probably need more info. mysql2 connection object ( mysqljs/mysql ) does not allow 'restarting', that is it's lifecycle is 'create, connect ( auth handshake ), commands, close'. Could be that the way you use it tries to initiate handshake twice?

Could you pass debug: true to driver options and check whats logged?

sidorares avatar Jul 09 '17 21:07 sidorares

All request is ok, but after 30-35 request, the app give those errors. This app is to be an API (nodejs/express), so I need that it support many request concurrently. I put POOL whith max to 1 becouse I need the same connection_id() so created temporary table will be Available on a second request.

Packages:

  • mysql: 5.7.16 (x86_64)
  • node mysql2: 1.3.5;
  • nodejs: v8.1.3;

Errors:

 GET /api/v1/4cxUeOu0VL6T7NhiZ5FxXTvaiDqDEd/16/products?customerNumber=103 201 54.066 ms - 210
 { method: 'raw',
   sql: '\n            DROP TEMPORARY\n            TABLE IF EXISTS\n            bucket_5.ProductsByCustomer\n        ',
   bindings: [],
   options: {},
   __knexQueryUid: 'dcd56663-7dd2-4636-bac0-524997d8dedb' }
 [ { method: 'raw',
     sql: 'CREATE TEMPORARY TABLE bucket_5.ProductsByCustomer comment=\'temporary_table_by_user\' select FlatTable.productLine, avg(FlatTable.totalSale) as totalSale from bucket_5.FlatTable where FlatTable.customerNumber = \'103\' group by FlatTable.productLine limit 10',
     bindings: [],
     options: {},
     __knexQueryUid: '423d4980-94d2-4d9e-abdc-6a21e4999efb' } ]
 { method: 'raw',
   sql: '\n            DROP TEMPORARY\n            TABLE IF EXISTS\n            bucket_5.remove\n        ',
   bindings: [],
   options: {},
   __knexQueryUid: 'e439b8c4-60e4-4aef-9eec-8cb167e420ec' }
 [ { method: 'raw',
     sql: 'CREATE TEMPORARY TABLE bucket_5.remove comment=\'temporary_table_by_user\' select FlatTable.customerNumber from bucket_5.FlatTable',
     bindings: [],
     options: {},
     __knexQueryUid: '3e8c41af-c4d3-4d39-b967-b25a2b1bef89' } ]
 { method: 'select',
   options: {},
   timeout: false,
   cancelOnTimeout: false,
   bindings: [ 10 ],
   __knexQueryUid: 'd1fb2ab1-c5c9-4d54-9796-b32e791ee698',
   sql: 'select * from `bucket_5`.`ProductsByCustomer` limit ?' }
 GET /api/v1/4cxUeOu0VL6T7NhiZ5FxXTvaiDqDEd/16/products?customerNumber=103 201 49.026 ms - 210
 /Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:127
       connection.threadId = handshakeCommand.handshake.connectionId;
                                                       ^
 
 TypeError: Cannot read property 'connectionId' of null
     at ClientHandshake.<anonymous> (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:127:55)
     at emitNone (events.js:105:13)
     at ClientHandshake.emit (events.js:207:7)
     at ClientHandshake.Command.execute (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/commands/command.js:34:12)
     at Connection.handlePacket (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:500:28)
     at PacketParser.onPacket (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:94:16)
     at PacketParser.executeStart (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/packet_parser.js:77:14)
     at Socket.<anonymous> (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:102:29)
     at emitOne (events.js:115:13)
     at Socket.emit (events.js:210:7)
     at addChunk (_stream_readable.js:252:12)
     at readableAddChunk (_stream_readable.js:239:11)
     at Socket.Readable.push (_stream_readable.js:197:10)
     at TCP.onread (net.js:589:20)
 [nodemon] app crashed - waiting for file changes before starting...

calebeaires avatar Jul 10 '17 11:07 calebeaires

POOL whith max to 1 becouse I need the same connection_id()

why pool then?

would you be able to put console.log(err) here https://github.com/sidorares/node-mysql2/blob/e8d185340e2280594efd1de162060ce29921cf45/lib/commands/command.js#L33 ?

looks like connection time error is not handled correctly

sidorares avatar Jul 10 '17 12:07 sidorares

If I remove pool options from the connection, the lib will create many connection_id(). With this behavior, I cant create temporary table to retrieve it later to be used on the next connection.

Here is the console.log

error to github issue { Error: Too many connections
     at Packet.asError (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/packets/packet.js:701:13)
     at ClientHandshake.Command.execute (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/commands/command.js:28:22)
     at Connection.handlePacket (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:500:28)
     at PacketParser.onPacket (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:94:16)
     at PacketParser.executeStart (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/packet_parser.js:77:14)
     at Socket.<anonymous> (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:102:29)
     at emitOne (events.js:115:13)
     at Socket.emit (events.js:210:7)
     at addChunk (_stream_readable.js:252:12)
     at readableAddChunk (_stream_readable.js:239:11)
     at Socket.Readable.push (_stream_readable.js:197:10)
     at TCP.onread (net.js:589:20) code: 'ER_CON_COUNT_ERROR', errno: 1040, sqlState: '' }
 /Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:127
       connection.threadId = handshakeCommand.handshake.connectionId;
                                                       ^
 
 TypeError: Cannot read property 'connectionId' of null
     at ClientHandshake.<anonymous> (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:127:55)
     at emitNone (events.js:105:13)
     at ClientHandshake.emit (events.js:207:7)
     at ClientHandshake.Command.execute (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/commands/command.js:35:12)
     at Connection.handlePacket (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:500:28)
     at PacketParser.onPacket (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:94:16)
     at PacketParser.executeStart (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/packet_parser.js:77:14)
     at Socket.<anonymous> (/Users/macbook/Documents/app/app-one/node_modules/mysql2/lib/connection.js:102:29)
     at emitOne (events.js:115:13)
     at Socket.emit (events.js:210:7)
     at addChunk (_stream_readable.js:252:12)
     at readableAddChunk (_stream_readable.js:239:11)
     at Socket.Readable.push (_stream_readable.js:197:10)
     at TCP.onread (net.js:589:20)

calebeaires avatar Jul 10 '17 12:07 calebeaires

Thanks for the log!

2 problems here:

  1. for some reason you are creating too many connections. Maybe you are using one connection (inside pool) to perform few related queries per http request and then don't close it, not sure

  2. driver does not handle connection error correctly ( handshake 'end' handler assumes there was handshake initialisation object which does not exist in case of error ). I need to fix this

If I remove pool options from the connection, the lib will create many connection_id()

you can just open connection and continue to use it across requests if you really need that

sidorares avatar Jul 10 '17 13:07 sidorares

KnexJs does not an alternative to pooling. The lib is very useful to the complexity of the app Gaio (connect to Oracle, MSSQL, MySQL).

Maybe I am missing something, but I couldn't find on it`s documentation. I have tried to not use pool, but the lib seems to close this door.

calebeaires avatar Jul 10 '17 14:07 calebeaires

ok sorry I forgot you are using it from knex

still, the problem is that you create new connection per profile and then not closing it. I suggest to refactor your db code not to rely on a connectionId / temp tables ( there are many solutions to this ) and just use one global knex object with pool max set to some reasonable number ( depending on avg query execution time / server size / your load that 'reasonable number' could be anything from 2 to 100s )

sidorares avatar Jul 10 '17 22:07 sidorares

knex.destroy([callback])

sidorares avatar Jul 10 '17 23:07 sidorares

@sidorares Thanks for your response and thanks for opened pull request. I changed the way to keep connection using pure mysql2 module (not knex) and finish it as soon response is delivered.

import { Me } from '../shared/Me';
const mysql = require('mysql2');

export class BucketDb {

    private static _connection = {};

    public static connect(connectionData) {

        const self = this;

        if (!this._connection || (this._connection && !this._connection[Me.profile().sessionId])) {
            this.instance(connectionData);
        }

        return this._connection[Me.profile().sessionId];

    }

    private static instance(connectionData) {
        this._connection[Me.profile().sessionId] = mysql.createConnection(connectionData);
    }

    public static end() {
        if (this._connection && this._connection[Me.profile().sessionId]) {
            this._connection[Me.profile().sessionId].end();
        }
    }

}

The errors persist like the echo above, but you have said that there are many solutions to this. May you give me a clue on how to make it happen? With those changes, here is the new error.

 GET /api/v1/4cxUeOu0VL6T7NhiZ5FxXTvaiDqDEd/16/products?customerNumber=103 501 2.524 ms - 101
 error to github issue { Error: Too many connections
     at Packet.asError (/Users/calebeaires/Documents/app/app-one/node_modules/mysql2/lib/packets/packet.js:701:13)
     at ClientHandshake.Command.execute (/Users/calebeaires/Documents/app/app-one/node_modules/mysql2/lib/commands/command.js:28:22)
     at Connection.handlePacket (/Users/calebeaires/Documents/app/app-one/node_modules/mysql2/lib/connection.js:515:28)
     at PacketParser.onPacket (/Users/calebeaires/Documents/app/app-one/node_modules/mysql2/lib/connection.js:94:16)
     at PacketParser.executeStart (/Users/calebeaires/Documents/app/app-one/node_modules/mysql2/lib/packet_parser.js:77:14)
     at Socket.<anonymous> (/Users/calebeaires/Documents/app/app-one/node_modules/mysql2/lib/connection.js:102:29)
     at emitOne (events.js:115:13)
     at Socket.emit (events.js:210:7)
     at addChunk (_stream_readable.js:252:12)
     at readableAddChunk (_stream_readable.js:239:11)
     at Socket.Readable.push (_stream_readable.js:197:10)
     at TCP.onread (net.js:589:20) code: 'ER_CON_COUNT_ERROR', errno: 1040, sqlState: '' }

calebeaires avatar Jul 11 '17 11:07 calebeaires

why do you need unique connection per Me.profile().sessionId? Can you just re-use one single connection for all sessions?

sidorares avatar Jul 11 '17 13:07 sidorares

Nope! This app process its data via mysql using create with select to generate analytical data so it can be used on dashboard or another tasks.

Each user can make use of different parameters to filter data from an table to generate another, so this generated table may have different data (per user need).

The temporary table solution make possible that each user can access those generated table based on the parameter that the current user set.

So, the session schema must be on the connection database itself, not on the node app on a way that on the same constructed workflow with the same tables names, each user will get different data because it were saved on the MySQL/MemSQL session he is.

With Me.profile().sessionId I can make an global object to save connection per user, not on a session connection by the app.

Here is how the process works https://youtu.be/vnaauWTzH_4?t=1m22s

calebeaires avatar Jul 11 '17 16:07 calebeaires

I found out that I didnt call connection.connect();as soon as an connection were created. Doing an query without globallyconnection.connect();` make each mysql2 request count as a new connection.


import { Me } from '../shared/Me';
const mysql = require('mysql2');

export class BucketDb {

    private static _connection = {};

    public static connect(connectionData) {

        const self = this;

        if (!this._connection || (this._connection && !this._connection[Me.profile().sessionId])) {
            this.instance(connectionData);
        }

        return this._connection[Me.profile().sessionId];

    }

    private static instance(connectionData) {
        connectionData['connectAttributes'] = {}
        connectionData['connectAttributes']['sessionId'] = Me.profile().sessionId;
        this._connection[Me.profile().sessionId] = mysql.createConnection(connectionData);
        this._connection[Me.profile().sessionId].connect();
    }

    public static end() {
        if (this._connection && this._connection[Me.profile().sessionId]) {
            this._connection[Me.profile().sessionId].end();
        }
    }

}

calebeaires avatar Jul 11 '17 18:07 calebeaires

I found out that I didnt callconnection.connect();as soon as an connection were created. Doing an query without globallyconnection.connect();` make each mysql2 request count as a new connection.

no, new connection is created per mysql.createConnection(connectionData);. Connection is created immediately, even if you don't call .connect()

sidorares avatar Jul 12 '17 05:07 sidorares

So, the session schema must be on the connection database itself, not on the node app on a way that on the same constructed workflow with the same tables names, each user will get different data because it were saved on the MySQL/MemSQL session he is.

I think it's a bad DB design. Mysql does not like too many connections, and typically connection limit is st to 100. Each new connection lives in own os thread with all related overhead - usually >2mb stack space per thread, context switching etc etc.

"other solutions" might be: store temp data in redis or have it in mysql but in one (non-temporary) single table with session id as one of the keys

sidorares avatar Jul 12 '17 05:07 sidorares

At least there is an error. I observed stuck request select 1(zero errors) through local unix socket connection. show processlist shows that my connections are sleeping.

alexey-sh avatar Dec 07 '23 23:12 alexey-sh