/**
* mysql-mcp - Connection Pool Manager
*
* Wraps mysql2 connection pooling with health monitoring,
* statistics tracking, and graceful shutdown support.
*/
import mysql from 'mysql2/promise';
import type { Pool, PoolConnection } from 'mysql2/promise';
import type { PoolConfig, PoolStats, HealthStatus } from '../types/index.js';
import { PoolError, ConnectionError } from '../types/index.js';
import { logger } from '../utils/logger.js';
/**
* Connection pool configuration with defaults
*/
export interface ConnectionPoolConfig {
host: string;
port: number;
user: string;
password: string;
database: string;
pool?: PoolConfig;
ssl?: boolean | mysql.SslOptions;
charset?: string;
timezone?: string;
connectTimeout?: number;
}
/**
* Connection pool wrapper with statistics and health monitoring
*/
export class ConnectionPool {
private pool: Pool | null = null;
private config: ConnectionPoolConfig;
private stats: PoolStats = {
total: 0,
active: 0,
idle: 0,
waiting: 0,
totalQueries: 0
};
private isShuttingDown = false;
constructor(config: ConnectionPoolConfig) {
this.config = config;
}
/**
* Initialize the connection pool
*/
async initialize(): Promise<void> {
if (this.pool) {
logger.warn('Connection pool already initialized');
return;
}
const poolConfig = this.config.pool ?? {};
try {
this.pool = mysql.createPool({
host: this.config.host,
port: this.config.port,
user: this.config.user,
password: this.config.password,
database: this.config.database,
// Pool options with defaults
connectionLimit: poolConfig.connectionLimit ?? 10,
waitForConnections: poolConfig.waitForConnections ?? true,
queueLimit: poolConfig.queueLimit ?? 0,
// Connection options
connectTimeout: this.config.connectTimeout ?? 10000,
charset: this.config.charset ?? 'utf8mb4',
timezone: this.config.timezone ?? 'local',
// SSL - convert boolean to mysql2 compatible format
// mysql2 expects string | SslOptions | undefined, not boolean
ssl: typeof this.config.ssl === 'boolean'
? (this.config.ssl ? {} : undefined)
: this.config.ssl,
// Other options
enableKeepAlive: poolConfig.enableKeepAlive ?? true,
keepAliveInitialDelay: poolConfig.keepAliveInitialDelay ?? 0,
// Namedplaceholders for better parameter handling
namedPlaceholders: false,
// Promise wrapper
Promise: Promise
});
// Test the connection
const connection = await this.pool.getConnection();
connection.release();
logger.info('Connection pool initialized', {
host: this.config.host,
port: this.config.port,
database: this.config.database,
connectionLimit: poolConfig.connectionLimit ?? 10
});
} catch (error) {
const err = error as Error;
throw new ConnectionError(`Failed to initialize connection pool: ${err.message}`, {
host: this.config.host,
port: this.config.port,
database: this.config.database
});
}
}
/**
* Get a connection from the pool
*/
async getConnection(): Promise<PoolConnection> {
if (!this.pool) {
throw new PoolError('Connection pool not initialized');
}
if (this.isShuttingDown) {
throw new PoolError('Connection pool is shutting down');
}
try {
const connection = await this.pool.getConnection();
this.stats.active++;
return connection;
} catch (error) {
const err = error as Error;
throw new PoolError(`Failed to get connection: ${err.message}`);
}
}
/**
* Release a connection back to the pool
*/
releaseConnection(connection: PoolConnection): void {
try {
connection.release();
this.stats.active = Math.max(0, this.stats.active - 1);
} catch (error) {
logger.error('Error releasing connection', { error: String(error) });
}
}
/**
* Execute a query using a pooled connection
* Returns full result tuple [rows, fields] for compatibility with rawQuery
*/
async query<T = unknown>(sql: string, params?: unknown[]): Promise<[T, mysql.FieldPacket[]]> {
if (!this.pool) {
throw new PoolError('Connection pool not initialized');
}
this.stats.totalQueries++;
try {
const result = await this.pool.query(sql, params);
return result as [T, mysql.FieldPacket[]];
} catch (error) {
const err = error as Error & { code?: string };
throw new PoolError(`Query failed: ${err.message}`, { sql }, err.code);
}
}
/**
* Execute a query and return full result with metadata
*/
async execute<T = unknown>(sql: string, params?: unknown[]): Promise<[T, mysql.FieldPacket[]]> {
if (!this.pool) {
throw new PoolError('Connection pool not initialized');
}
this.stats.totalQueries++;
try {
const result = await this.pool.execute(sql, params);
return result as [T, mysql.FieldPacket[]];
} catch (error) {
const err = error as Error & { code?: string };
throw new PoolError(`Execute failed: ${err.message}`, { sql }, err.code);
}
}
/**
* Get pool statistics
*/
getStats(): PoolStats {
if (!this.pool) {
return this.stats;
}
// mysql2 pool doesn't expose detailed stats directly
// We track what we can
return {
...this.stats,
total: this.config.pool?.connectionLimit ?? 10,
idle: (this.config.pool?.connectionLimit ?? 10) - this.stats.active
};
}
/**
* Check pool health
*/
async checkHealth(): Promise<HealthStatus> {
if (!this.pool) {
return {
connected: false,
error: 'Pool not initialized'
};
}
const startTime = Date.now();
try {
const connection = await this.pool.getConnection();
try {
// Ping to verify connection
await connection.ping();
// Get server version
const [rows] = await connection.query('SELECT VERSION() as version');
const result = rows as { version: string }[];
const version = result[0]?.version;
const latencyMs = Date.now() - startTime;
return {
connected: true,
latencyMs,
version,
poolStats: this.getStats()
};
} finally {
connection.release();
}
} catch (error) {
return {
connected: false,
latencyMs: Date.now() - startTime,
error: String(error),
poolStats: this.getStats()
};
}
}
/**
* Gracefully shutdown the pool
*/
async shutdown(): Promise<void> {
if (!this.pool) {
return;
}
this.isShuttingDown = true;
logger.info('Shutting down connection pool...');
try {
await this.pool.end();
this.pool = null;
logger.info('Connection pool shut down successfully');
} catch (error) {
logger.error('Error shutting down connection pool', { error: String(error) });
throw error;
}
}
/**
* Check if pool is initialized
*/
isInitialized(): boolean {
return this.pool !== null;
}
/**
* Check if pool is shutting down
*/
isClosing(): boolean {
return this.isShuttingDown;
}
}