@slsplus/database
Version:
Serverless Database Tool
392 lines (391 loc) • 11.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Database = void 0;
const promise_1 = require("mysql2/promise");
const utils_1 = require("../utils");
const ConnectionErrors = [
'ER_TOO_MANY_USER_CONNECTIONS',
'ER_CON_COUNT_ERROR',
'ER_USER_LIMIT_REACHED',
'ER_OUT_OF_RESOURCES',
'ER_CON_COUNT_ERROR',
'PROTOCOL_CONNECTION_LOST',
'PROTOCOL_SEQUENCE_TIMEOUT',
'ETIMEDOUT',
];
const DefaultOptions = {
debug: false,
maxRetries: 50,
sleepGap: 100,
getMaxConnsFreq: 15 * 1000,
getUsedConnsFreq: 0,
connsUtilization: 0.8,
zombieMinTimeout: 3,
zombieMaxTimeout: 15 * 60,
onConnect: (client) => {
return client;
},
onClear: (zombie) => {
return zombie;
},
onConnectError: (e) => {
throw e;
},
onClearError: (e) => {
throw e;
},
onError: (e) => {
throw e;
},
onClose: () => {
// no op\
},
};
class Transaction {
constructor(db) {
this.queries = [];
this.db = db;
this.queries = [];
this.rollback = () => {
// no op
};
}
addQuery({ sql, values }) {
this.queries.push({
sql,
values,
});
}
setRollback(fn) {
if (fn && utils_1.typeOf(fn) === 'Function') {
this.rollback = fn;
}
}
async commit() {
return await this.db.commit(this.queries, this.rollback);
}
}
class Database {
/**
* constructor
* @param connOptions mysql connection options
* @param options database tool options
*/
constructor(connOptions, options) {
// database connection
this.client = null;
// retry to times
this.retries = 0;
// error counts
this.errors = 0;
// max connections
this.maxConnections = {
updated: 0,
total: 0,
userLimit: false,
};
// used connections
this.usedConnections = {
updated: 0,
total: 0,
maxAge: 0,
};
this.options = {
...DefaultOptions,
...(options || {}),
};
this.connectionOptions = connOptions;
}
/**
* reset retry counts
*/
resetRetries() {
this.retries = 0;
}
// reset client
resetClient() {
this.client = null;
}
// try to connect to database
async connect() {
try {
await this._connect();
}
catch (e) {
if (ConnectionErrors.indexOf(e.code) && this.retries < this.options.maxRetries) {
this.retries++;
await utils_1.sleep(this.options.sleepGap);
await this.connect();
}
else {
this.options.onConnectError(e);
}
}
}
/**
* query databse
* @param sql query statament
* @param values query parameters
*/
async query({ sql, values, rollback, }) {
await this.connect();
if (this.client) {
try {
const res = await this._dbQuery({ sql, values });
return res;
}
catch (e) {
if (this.options.debug) {
console.log('[QUERY ERROR] ', e);
}
if (e.code === 'PROTOCOL_SEQUENCE_TIMEOUT') {
await this.client.destroy();
this.resetClient();
throw e;
}
else if (/^PROTOCOL_ENQUEUE_AFTER_/.test(e.code) ||
e.code === 'PROTOCOL_CONNECTION_LOST' ||
e.code === 'EPIPE') {
this.resetClient();
return this.query({ sql, values });
}
else {
// rollback for transaction
if (rollback) {
await this.query({ sql: 'ROLLBACK' });
rollback(e);
}
throw e;
}
}
}
return {};
}
/**
* create trarnsaction
*/
createTransaction() {
return new Transaction(this);
}
/**
* commit transaction queries
* @param queries sql queries
* @param rollback rollback function
*/
async commit(queries, rollback) {
const results = [];
if (this.client) {
await this.query({ sql: `START TRANSACTION` });
for (let i = 0; i < queries.length; i++) {
const { sql, values } = queries[i];
const res = await this.query({ sql, values, rollback });
results.push(res);
}
await this.query({ sql: 'COMMIT' });
}
return results;
}
/**
* close connection
*/
async close() {
if (this.client) {
await this.client.end();
this.resetClient();
this.options.onClose();
}
}
/**
* end client, clear all zombies
*/
async end() {
if (this.client !== null) {
await this._getMaxConnections();
await this._getTotalConnections();
const currentUtilization = this.usedConnections.total / this.maxConnections.total;
// if over utilization threshold, clean zombies
if (currentUtilization > this.options.connsUtilization) {
const timeout = Math.min(Math.max(this.usedConnections.maxAge, this.options.zombieMinTimeout), this.options.zombieMaxTimeout);
// clear zombies, if they are whin the timeout
const clearedZombies = await this._clearZombieConnections(timeout);
// if no zombies were cleared, close this connection
if (clearedZombies === 0) {
await this.close();
}
}
else if (this.usedConnections.maxAge > this.options.zombieMaxTimeout) {
// if zombies exist that are more then max zombie timeout, clear them
await this._clearZombieConnections(this.options.zombieMaxTimeout);
}
}
}
/**
* create database
* @param dbname database name
*/
async createDb(dbname) {
const res = await this.query({ sql: `CREATE DATABASE IF NOT EXISTS ${dbname};` });
await this.query({ sql: `USE ${dbname};` });
return res;
}
/**
* truncate table
* @param dbname table name
*/
async truncateTable(tableName) {
return this.query({ sql: `TRUNCATE ${tableName};` });
}
/**
* get database list
*/
async getDbs() {
const databases = await this.query({ sql: `SHOW DATABASES;` });
return (databases || []);
}
/**
*
* @param dbname database name
*/
async isDbExist(dbname) {
const dbs = await this.getDbs();
const [exist] = dbs.filter((item) => item.Database === dbname);
return !!exist;
}
/**
* get tables
*/
async getTables() {
const tables = await this.query({ sql: `SHOW TABLES;` });
return (tables || []);
}
/**
* whether table exist
* @param dbname database name
* @param tableName table name
*/
async isTableExist(dbname, tableName) {
const tables = await this.getTables();
const [exist] = tables.filter((item) => {
return item[`Tables_in_${dbname}`] === tableName;
});
return !!exist;
}
// create databse connection
async _connect() {
if (!this.client) {
try {
this.client = (await promise_1.createConnection(this.connectionOptions));
this.resetRetries();
this.options.onConnect(this.client);
this.client.on('error', async (err) => {
this.errors++;
this.resetClient();
this.options.onError(err);
});
return true;
}
catch (e) {
this.resetClient();
throw e;
}
}
else {
try {
const { connecting } = this.client.connection.stream;
if (!connecting) {
await this.client.connect();
}
}
catch (e) {
console.log('[CONNECT ERROR] ', e);
}
}
}
/**
* get max connections
*/
async _getMaxConnections() {
if (this.client) {
if (Date.now() - this.maxConnections.updated > this.options.getMaxConnsFreq) {
const results = await this._dbQuery({
sql: `SELECT IF (@ > 0,
LEAST(@,@),
@) AS total,
IF(@ > 0,true,false) AS userLimit`,
});
if (results) {
this.maxConnections = {
total: results[0].total || 0,
userLimit: results[0].userLimit === 1 ? true : false,
updated: Date.now(),
};
}
}
}
}
/**
* get totol connections
*/
async _getTotalConnections() {
if (this.client) {
if (Date.now() - this.usedConnections.updated > this.options.getUsedConnsFreq) {
const results = await this._dbQuery({
sql: `SELECT COUNT(ID) as total, MAX(time) as max_age
FROM information_schema.processlist
WHERE (user = ? AND @ > 0) OR true`,
values: [this.connectionOptions.user],
});
if (results) {
this.usedConnections = {
total: results[0].total || 0,
updated: Date.now(),
maxAge: results[0].max_age || 0,
};
}
}
}
}
/**
* clear zombie connections
* @param timeout timeout for sleepy connections
*/
async _clearZombieConnections(timeout) {
let clearedZombies = 0;
if (timeout > this.usedConnections.maxAge || this.client === null) {
return clearedZombies;
}
const zombieConnections = await this._dbQuery({
sql: `SELECT ID,time FROM information_schema.processlist
WHERE command = 'Sleep' AND time >= ? and user = ?
ORDER BY time DESC`,
values: [!isNaN(timeout) ? timeout : 900, this.connectionOptions.user],
});
if (zombieConnections) {
for (let i = 0; i < zombieConnections.length; i++) {
try {
const zombie = zombieConnections[i];
this._dbQuery({ sql: `KILL ?`, values: zombie.ID });
this.options.onClear(zombie);
clearedZombies++;
}
catch (e) {
this.options.onClearError(e);
}
}
}
return clearedZombies;
}
async _dbQuery({ sql, values }) {
if (this.client) {
const cmdSql = this.client.format(sql, values);
const [res] = await this.client.query(cmdSql);
if (this.options.debug) {
console.log(`[Query]: ${cmdSql}`);
console.log(`[RESULT] ${JSON.stringify(res)}`);
}
return res;
}
return this.query({ sql, values });
}
}
exports.Database = Database;