Skip to content

Commit d500518

Browse files
committed
Store Compiled Packet Parsers in a global cache - Resolves #722
When a connection is closed, any compiled parser created for that will be lost, as the connection is removed. However, Node/V8 Internals appear to be holding onto any created function. This can result in memory leaking based on connection cycling and new parsers being created again. This change creates a global cache store since everything around the generated code is not connection specific. This also completes a TODO to use an LRU cache, although I set a high value as to avoid running into the leak if the LRU is too small. An API is provided for anyone who does need to control the max value, as well as an API to clear the parser cache.
1 parent b6175d8 commit d500518

File tree

7 files changed

+87
-60
lines changed

7 files changed

+87
-60
lines changed

index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ var SqlString = require('sqlstring');
22

33
var Connection = require('./lib/connection.js');
44
var ConnectionConfig = require('./lib/connection_config.js');
5+
var parserCache = require("./lib/parsers/parser_cache");
56

67
module.exports.createConnection = function(opts) {
78
return new Connection({ config: new ConnectionConfig(opts) });
@@ -61,3 +62,11 @@ exports.__defineGetter__('Charsets', function() {
6162
exports.__defineGetter__('CharsetToEncoding', function() {
6263
return require('./lib/constants/charset_encodings.js');
6364
});
65+
66+
exports.setMaxParserCache = function (max) {
67+
parserCache.setMaxCache(max);
68+
};
69+
70+
exports.clearParserCache = function () {
71+
parserCache.clearCache();
72+
};

lib/commands/execute.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var Packets = require('../packets/index.js');
66

77
var objectAssign = require('object-assign');
88

9-
var compileParser = require('../compile_binary_parser.js');
9+
var getBinaryParser = require('../parsers/binary_parser.js');
1010

1111
function Execute(options, callback) {
1212
Command.call(this);
@@ -33,13 +33,7 @@ function Execute(options, callback) {
3333
util.inherits(Execute, Command);
3434

3535
Execute.prototype.buildParserFromFields = function(fields, connection) {
36-
var parserKey = connection.keyFromFields(fields, this.options);
37-
var parser = connection.binaryProtocolParsers[parserKey];
38-
if (!parser) {
39-
parser = compileParser(fields, this.options, connection.config);
40-
connection.binaryProtocolParsers[parserKey] = parser;
41-
}
42-
return parser;
36+
return getBinaryParser(fields, this.options, connection.config);
4337
};
4438

4539
Execute.prototype.start = function(packet, connection) {

lib/commands/query.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ var objectAssign = require('object-assign');
77

88
var Command = require('./command.js');
99
var Packets = require('../packets/index.js');
10-
var compileParser = require('../compile_text_parser.js');
10+
var getTextParser = require('../parsers/text_parser.js');
1111
var ServerStatus = require('../constants/server_status.js');
1212
var CharsetToEncoding = require('../constants/charset_encodings.js');
1313

@@ -195,15 +195,10 @@ Query.prototype.readField = function(packet, connection) {
195195
}
196196

197197
// last field received
198-
if (this._receivedFieldsCount == this._fieldCount) {
198+
if (this._receivedFieldsCount === this._fieldCount) {
199199
var fields = this._fields[this._resultIndex];
200200
this.emit('fields', fields);
201-
var parserKey = connection.keyFromFields(fields, this.options);
202-
this._rowParser = connection.textProtocolParsers[parserKey];
203-
if (!this._rowParser) {
204-
this._rowParser = compileParser(fields, this.options, connection.config);
205-
connection.textProtocolParsers[parserKey] = this._rowParser;
206-
}
201+
this._rowParser = getTextParser(fields, this.options, connection.config);
207202
return Query.prototype.fieldsEOF;
208203
}
209204
return Query.prototype.readField;

lib/connection.js

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,6 @@ function Connection(opts) {
5858
}
5959
});
6060

61-
// TODO: make it lru cache
62-
// https://github.com/mercadolibre/node-simple-lru-cache
63-
// or https://github.com/rsms/js-lru
64-
// or https://github.com/monsur/jscache
65-
// or https://github.com/isaacs/node-lru-cache
66-
//
67-
// key is field.name + ':' + field.columnType + ':' field.flags + '/'
68-
this.textProtocolParsers = {};
69-
70-
// TODO: not sure if cache should be separate (same key as with textProtocolParsers)
71-
// or part of prepared statements cache (key is sql query)
72-
this.binaryProtocolParsers = {};
73-
7461
this.serverCapabilityFlags = 0;
7562
this.authorized = false;
7663

@@ -639,25 +626,6 @@ Connection.prototype.resume = function resume() {
639626
this.stream.resume();
640627
};
641628

642-
Connection.prototype.keyFromFields = function keyFromFields(fields, options) {
643-
var res =
644-
typeof options.nestTables +
645-
'/' +
646-
options.nestTables +
647-
'/' +
648-
options.rowsAsArray +
649-
options.supportBigNumbers +
650-
'/' +
651-
options.bigNumberStrings +
652-
'/' +
653-
typeof options.typeCast;
654-
for (var i = 0; i < fields.length; ++i) {
655-
res +=
656-
'/' + fields[i].name + ':' + fields[i].columnType + ':' + fields[i].flags;
657-
}
658-
return res;
659-
};
660-
661629
Connection.statementKey = function(options) {
662630
return (
663631
typeof options.nestTables +

lib/compile_binary_parser.js renamed to lib/parsers/binary_parser.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
var FieldFlags = require('./constants/field_flags.js');
2-
var Charsets = require('./constants/charsets.js');
3-
var CharsetToEncoding = require('./constants/charset_encodings.js');
4-
var Types = require('./constants/types.js');
5-
var srcEscape = require('./helpers').srcEscape;
1+
var FieldFlags = require('../constants/field_flags.js');
2+
var Charsets = require('../constants/charsets.js');
3+
var CharsetToEncoding = require('../constants/charset_encodings.js');
4+
var Types = require('../constants/types.js');
5+
var srcEscape = require('../helpers').srcEscape;
66
var genFunc = require('generate-function');
7-
7+
var parserCache = require('./parser_cache.js');
88
var typeNames = [];
99
for (var t in Types) {
1010
typeNames[Types[t]] = t;
@@ -181,4 +181,8 @@ function readCodeFor(field, config, options, fieldNum) {
181181
}
182182
}
183183

184-
module.exports = compile;
184+
function getBinaryParser(fields, options, config) {
185+
return parserCache.getParser('binary', fields, options, config, compile);
186+
}
187+
188+
module.exports = getBinaryParser;

lib/parsers/parser_cache.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
var LRU = require('lru-cache');
2+
3+
var parserCache = new LRU({
4+
max: 15000
5+
});
6+
7+
function keyFromFields(type, fields, options) {
8+
var res =
9+
type +
10+
'/' +
11+
typeof options.nestTables +
12+
'/' +
13+
options.nestTables +
14+
'/' +
15+
options.rowsAsArray +
16+
options.supportBigNumbers +
17+
'/' +
18+
options.bigNumberStrings +
19+
'/' +
20+
typeof options.typeCast;
21+
for (var i = 0; i < fields.length; ++i) {
22+
res +=
23+
'/' + fields[i].name + ':' + fields[i].columnType + ':' + fields[i].flags;
24+
}
25+
return res;
26+
}
27+
28+
function getParser(type, fields, options, config, compiler) {
29+
var key = keyFromFields(type, fields, options);
30+
var parser = parserCache.get(key);
31+
32+
if (parser) {
33+
return parser;
34+
}
35+
36+
parser = compiler(fields, options, config);
37+
parserCache.set(key, parser);
38+
return parser;
39+
}
40+
41+
function setMaxCache(max) {
42+
parserCache.max = max;
43+
}
44+
45+
function clearCache() {
46+
parserCache.reset();
47+
}
48+
49+
module.exports = {
50+
getParser: getParser,
51+
setMaxCache: setMaxCache,
52+
clearCache: clearCache
53+
};

lib/compile_text_parser.js renamed to lib/parsers/text_parser.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
var Types = require('./constants/types.js');
2-
var Charsets = require('./constants/charsets.js');
3-
var CharsetToEncoding = require('./constants/charset_encodings.js');
4-
var srcEscape = require('./helpers').srcEscape;
1+
var Types = require('../constants/types.js');
2+
var Charsets = require('../constants/charsets.js');
3+
var CharsetToEncoding = require('../constants/charset_encodings.js');
4+
var srcEscape = require('../helpers').srcEscape;
55
var genFunc = require('generate-function');
6+
var parserCache = require('./parser_cache.js');
67

78
var typeNames = [];
89
for (var t in Types) {
@@ -189,4 +190,7 @@ function readCodeFor(type, charset, encodingExpr, config, options) {
189190
}
190191
}
191192

192-
module.exports = compile;
193+
function getTextParser(fields, options, config) {
194+
return parserCache.getParser('text', fields, options, config, compile);
195+
}
196+
module.exports = getTextParser;

0 commit comments

Comments
 (0)