diff --git a/src/database.js b/src/database.js index 94fbfe8e5..b7a7b2b34 100644 --- a/src/database.js +++ b/src/database.js @@ -14,7 +14,6 @@ exports = module.exports = { var assert = require('assert'), async = require('async'), - BoxError = require('./boxerror.js'), child_process = require('child_process'), constants = require('./constants.js'), debug = require('debug')('box:database'), @@ -22,8 +21,7 @@ var assert = require('assert'), once = require('once'), util = require('util'); -var gConnectionPool = null, - gDefaultConnection = null; +var gConnectionPool = null; const gDatabase = { hostname: '127.0.0.1', @@ -43,14 +41,16 @@ function initialize(callback) { gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim(); } + // https://github.com/mysqljs/mysql#pool-options gConnectionPool = mysql.createPool({ - connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions + connectionLimit: 5, host: gDatabase.hostname, user: gDatabase.username, password: gDatabase.password, port: gDatabase.port, database: gDatabase.name, multipleStatements: false, + waitForConnections: true, // getConnection() will wait until a connection is avaiable ssl: false, timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC }); @@ -58,55 +58,20 @@ function initialize(callback) { gConnectionPool.on('connection', function (connection) { // connection objects are re-used. so we have to attach to the event here (once) to prevent crash // note the pool also has an 'acquire' event but that is called whenever we do a getConnection() - connection.on('error', (error) => debug(`Connection ${connection.threadId} error: ${error.message}`)); + connection.on('error', (error) => debug(`Connection ${connection.threadId} error: ${error.message} ${error.code}`)); connection.query('USE ' + gDatabase.name); connection.query('SET SESSION sql_mode = \'strict_all_tables\''); }); - reconnect(callback); + callback(); } function uninitialize(callback) { - if (gConnectionPool) { - gConnectionPool.end(callback); - gDefaultConnection = null; - gConnectionPool = null; - } else { - callback(null); - } -} + if (!gConnectionPool) return callback(null); -function reconnect(callback) { - callback = callback ? once(callback) : function () {}; - - debug('reconnect: connecting to database'); - - gConnectionPool.getConnection(function (error, connection) { - if (error) { - debug(`reconnect: db connection error. ${error.message} fatal:${error.fatal} code:${error.code}. Will retry in 10 seconds`); - return setTimeout(reconnect.bind(null, callback), 10000); - } - - debug('reconnect: connected to database'); - - connection.on('error', function (error) { - // by design, we catch all normal errors by providing callbacks. - // this function should be invoked only when we have no callbacks pending and we have a fatal error - assert(error.fatal, 'Non-fatal error on connection object'); - - debug(`reconnect: db connection error. ${error.message} fatal:${error.fatal} code:${error.code}. Will retry in 10 seconds`); - - gDefaultConnection = null; - - // This is most likely an issue an can cause double callbacks from reconnect() - setTimeout(reconnect.bind(null, callback), 10000); - }); - - gDefaultConnection = connection; - - callback(null); - }); + gConnectionPool.end(callback); + gConnectionPool = null; } function clear(callback) { @@ -119,40 +84,12 @@ function clear(callback) { child_process.exec(cmd, callback); } -function beginTransaction(callback) { - assert.strictEqual(typeof callback, 'function'); - - if (gConnectionPool === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No database connection pool.')); - - gConnectionPool.getConnection(function (error, connection) { - if (error) return callback(error); - - connection.beginTransaction(function (error) { - if (error) return callback(error); - - return callback(null, connection); - }); - }); -} - function query() { var args = Array.prototype.slice.call(arguments); var callback = args[args.length - 1]; assert.strictEqual(typeof callback, 'function'); - if (gDefaultConnection === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No connection to database')); - - gDefaultConnection.query.apply(gDefaultConnection, args); -} - -function rollback(connection, transactionError, callback) { - connection.rollback(function (error) { - if (error) debug('rollback: error when rolling back', error); - - connection.release(); - - callback(transactionError); - }); + gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release } function transaction(queries, callback) { @@ -161,20 +98,26 @@ function transaction(queries, callback) { callback = once(callback); - beginTransaction(function (error, connection) { + gConnectionPool.getConnection(function (error, connection) { if (error) return callback(error); - async.mapSeries(queries, function iterator(query, done) { - connection.query(query.query, query.args, done); - }, function seriesDone(error, results) { - if (error) return rollback(connection, error, callback); + const releaseConnection = (error) => { connection.release(); callback(error); }; - connection.commit(function (error) { - if (error) return rollback(connection, error, callback); + connection.beginTransaction(function (error) { + if (error) return releaseConnection(error); - connection.release(); + async.mapSeries(queries, function iterator(query, done) { + connection.query(query.query, query.args, done); + }, function seriesDone(error, results) { + if (error) return connection.rollback(() => releaseConnection(error)); - callback(null, results); + connection.commit(function (error) { + if (error) return connection.rollback(() => releaseConnection(error)); + + connection.release(); + + callback(null, results); + }); }); }); });