四好公路
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

371 lines
11 KiB

'use strict';
const Pooling = require('generic-pool');
const Promise = require('../../promise');
const _ = require('lodash');
const Utils = require('../../utils');
const debug = Utils.getLogger().debugContext('pool');
const semver = require('semver');
const defaultPoolingConfig = {
max: 5,
min: 0,
idle: 10000,
acquire: 10000,
evict: 10000,
handleDisconnects: true
};
/**
* Abstract Connection Manager
*
* Connection manager which handles pool, replication and determining database version
* Works with generic-pool to maintain connection pool
*
* @private
*/
class ConnectionManager {
constructor(dialect, sequelize) {
const config = _.cloneDeep(sequelize.config);
this.sequelize = sequelize;
this.config = config;
this.dialect = dialect;
this.versionPromise = null;
this.dialectName = this.sequelize.options.dialect;
if (config.pool === false) {
throw new Error('Support for pool:false was removed in v4.0');
}
config.pool = _.defaults(config.pool || {}, defaultPoolingConfig, {
validate: this._validate.bind(this),
Promise
});
this.initPools();
}
refreshTypeParser(dataTypes) {
_.each(dataTypes, dataType => {
if (dataType.hasOwnProperty('parse')) {
if (dataType.types[this.dialectName]) {
this._refreshTypeParser(dataType);
} else {
throw new Error('Parse function not supported for type ' + dataType.key + ' in dialect ' + this.dialectName);
}
}
});
}
/**
* Handler which executes on process exit or connection manager shutdown
*
* @private
* @return {Promise}
*/
_onProcessExit() {
if (!this.pool) {
return Promise.resolve();
}
return this.pool.drain().then(() => {
debug('connection drain due to process exit');
return this.pool.clear();
});
}
/**
* Drain the pool and close it permanently
*
* @return {Promise}
*/
close() {
// Mark close of pool
this.getConnection = function getConnection() {
return Promise.reject(new Error('ConnectionManager.getConnection was called after the connection manager was closed!'));
};
return this._onProcessExit();
}
/**
* Initialize connection pool. By default pool autostart is set to false, so no connection will be
* be created unless `pool.acquire` is called.
*/
initPools() {
const config = this.config;
if (!config.replication) {
this.pool = Pooling.createPool({
create: () => this._connect(config).catch(err => err),
destroy: mayBeConnection => {
if (mayBeConnection instanceof Error) {
return Promise.resolve();
}
return this._disconnect(mayBeConnection)
.tap(() => { debug('connection destroy'); });
},
validate: config.pool.validate
}, {
Promise: config.pool.Promise,
testOnBorrow: true,
returnToHead: true,
autostart: false,
max: config.pool.max,
min: config.pool.min,
acquireTimeoutMillis: config.pool.acquire,
idleTimeoutMillis: config.pool.idle,
evictionRunIntervalMillis: config.pool.evict
});
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, no replication`);
return;
}
let reads = 0;
if (!Array.isArray(config.replication.read)) {
config.replication.read = [config.replication.read];
}
// Map main connection config
config.replication.write = _.defaults(config.replication.write, _.omit(config, 'replication'));
// Apply defaults to each read config
config.replication.read = _.map(config.replication.read, readConfig =>
_.defaults(readConfig, _.omit(this.config, 'replication'))
);
// custom pooling for replication (original author @janmeier)
this.pool = {
release: client => {
if (client.queryType === 'read') {
return this.pool.read.release(client);
} else {
return this.pool.write.release(client);
}
},
acquire: (priority, queryType, useMaster) => {
useMaster = _.isUndefined(useMaster) ? false : useMaster;
if (queryType === 'SELECT' && !useMaster) {
return this.pool.read.acquire(priority)
.then(mayBeConnection => this._determineConnection(mayBeConnection));
} else {
return this.pool.write.acquire(priority)
.then(mayBeConnection => this._determineConnection(mayBeConnection));
}
},
destroy: mayBeConnection => {
if (mayBeConnection.queryType === undefined) {
return Promise.all([
this.pool.read.destroy(mayBeConnection).catch(/Resource not currently part of this pool/, () => {}),
this.pool.write.destroy(mayBeConnection).catch(/Resource not currently part of this pool/, () => {})
]);
}
return this.pool[mayBeConnection.queryType].destroy(mayBeConnection);
},
clear: () => {
return Promise.join(
this.pool.read.clear(),
this.pool.write.clear()
).tap(() => { debug('all connection clear'); });
},
drain: () => {
return Promise.join(
this.pool.write.drain(),
this.pool.read.drain()
);
},
read: Pooling.createPool({
create: () => {
const nextRead = reads++ % config.replication.read.length; // round robin config
return this
._connect(config.replication.read[nextRead])
.tap(connection => {
connection.queryType = 'read';
})
.catch(err => err);
},
destroy: mayBeConnection => {
if (mayBeConnection instanceof Error) {
return Promise.resolve();
}
return this._disconnect(mayBeConnection)
.tap(() => { debug('connection destroy'); });
},
validate: config.pool.validate
}, {
Promise: config.pool.Promise,
testOnBorrow: true,
returnToHead: true,
autostart: false,
max: config.pool.max,
min: config.pool.min,
acquireTimeoutMillis: config.pool.acquire,
idleTimeoutMillis: config.pool.idle,
evictionRunIntervalMillis: config.pool.evict
}),
write: Pooling.createPool({
create: () => {
return this
._connect(config.replication.write)
.tap(connection => {
connection.queryType = 'write';
})
.catch(err => err);
},
destroy: mayBeConnection => {
if (mayBeConnection instanceof Error) {
return Promise.resolve();
}
return this._disconnect(mayBeConnection)
.tap(() => { debug('connection destroy'); });
},
validate: config.pool.validate
}, {
Promise: config.pool.Promise,
testOnBorrow: true,
returnToHead: true,
autostart: false,
max: config.pool.max,
min: config.pool.min,
acquireTimeoutMillis: config.pool.acquire,
idleTimeoutMillis: config.pool.idle,
evictionRunIntervalMillis: config.pool.evict
})
};
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, with replication`);
}
/**
* Get connection from pool. It sets database version if it's not already set.
* Call pool.acquire to get a connection
*
* @param {Object} [options] Pool options
* @param {Integer} [options.priority] Set priority for this call. Read more at https://github.com/coopernurse/node-pool#priority-queueing
* @param {String} [options.type] Set which replica to use. Available options are `read` and `write`
* @param {Boolean} [options.useMaster=false] Force master or write replica to get connection from
*
* @return {Promise<Connection>}
*/
getConnection(options) {
options = options || {};
let promise;
if (this.sequelize.options.databaseVersion === 0) {
if (this.versionPromise) {
promise = this.versionPromise;
} else {
promise = this.versionPromise = this._connect(this.config.replication.write || this.config)
.then(connection => {
const _options = {};
_options.transaction = {connection}; // Cheat .query to use our private connection
_options.logging = () => {};
_options.logging.__testLoggingFn = true;
return this.sequelize.databaseVersion(_options).then(version => {
const parsedVersion = _.get(semver.coerce(version), 'version') || version;
this.sequelize.options.databaseVersion = semver.valid(parsedVersion)
? parsedVersion
: this.defaultVersion;
this.versionPromise = null;
return this._disconnect(connection);
});
}).catch(err => {
this.versionPromise = null;
throw err;
});
}
} else {
promise = Promise.resolve();
}
return promise.then(() => {
return this.pool.acquire(options.priority, options.type, options.useMaster)
.then(mayBeConnection => this._determineConnection(mayBeConnection))
.tap(() => { debug('connection acquired'); });
});
}
/**
* Release a pooled connection so it can be utilized by other connection requests
*
* @param {Connection} connection
*
* @return {Promise}
*/
releaseConnection(connection) {
return this.pool.release(connection)
.tap(() => { debug('connection released'); })
.catch(/Resource not currently part of this pool/, () => {});
}
/**
* Check if something acquired by pool is indeed a connection but not an Error instance
* Why we need to do this https://github.com/sequelize/sequelize/pull/8330
*
* @param {Object|Error} mayBeConnection Object which can be either connection or error
*
* @return {Promise<Connection>}
*/
_determineConnection(mayBeConnection) {
if (mayBeConnection instanceof Error) {
return Promise.resolve(this.pool.destroy(mayBeConnection))
.catch(/Resource not currently part of this pool/, () => {})
.then(() => { throw mayBeConnection; });
}
return Promise.resolve(mayBeConnection);
}
/**
* Call dialect library to get connection
*
* @param {*} config Connection config
* @private
* @return {Promise<Connection>}
*/
_connect(config) {
return this.sequelize.runHooks('beforeConnect', config)
.then(() => this.dialect.connectionManager.connect(config))
.then(connection => this.sequelize.runHooks('afterConnect', connection, config).return(connection));
}
/**
* Call dialect library to disconnect a connection
*
* @param {Connection} connection
* @private
* @return {Promise}
*/
_disconnect(connection) {
return this.dialect.connectionManager.disconnect(connection);
}
/**
* Determine if a connection is still valid or not
*
* @param {Connection} connection
*
* @return {Boolean}
*/
_validate(connection) {
if (!this.dialect.connectionManager.validate) {
return true;
}
return this.dialect.connectionManager.validate(connection);
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;