239 lines
6.8 KiB
JavaScript
239 lines
6.8 KiB
JavaScript
const { Client } = require('ssh2');
|
|
const mysql = require('mysql2/promise');
|
|
const fs = require('fs');
|
|
|
|
// Connection pooling and cache configuration
|
|
const connectionCache = {
|
|
ssh: null,
|
|
dbConnection: null,
|
|
lastUsed: 0,
|
|
isConnecting: false,
|
|
connectionPromise: null,
|
|
// Cache expiration time in milliseconds (5 minutes)
|
|
expirationTime: 5 * 60 * 1000,
|
|
// Cache for query results (key: query string, value: {data, timestamp})
|
|
queryCache: new Map(),
|
|
// Cache duration for different query types in milliseconds
|
|
cacheDuration: {
|
|
'field-options': 30 * 60 * 1000, // 30 minutes for field options
|
|
'product-lines': 10 * 60 * 1000, // 10 minutes for product lines
|
|
'sublines': 10 * 60 * 1000, // 10 minutes for sublines
|
|
'taxonomy': 30 * 60 * 1000, // 30 minutes for taxonomy data
|
|
'default': 60 * 1000 // 1 minute default
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get a database connection with connection pooling
|
|
* @returns {Promise<{ssh: object, connection: object}>} The SSH and database connection
|
|
*/
|
|
async function getDbConnection() {
|
|
const now = Date.now();
|
|
|
|
// Check if we need to refresh the connection due to inactivity
|
|
const needsRefresh = !connectionCache.ssh ||
|
|
!connectionCache.dbConnection ||
|
|
(now - connectionCache.lastUsed > connectionCache.expirationTime);
|
|
|
|
// If connection is still valid, update last used time and return existing connection
|
|
if (!needsRefresh) {
|
|
connectionCache.lastUsed = now;
|
|
return {
|
|
ssh: connectionCache.ssh,
|
|
connection: connectionCache.dbConnection
|
|
};
|
|
}
|
|
|
|
// If another request is already establishing a connection, wait for that promise
|
|
if (connectionCache.isConnecting && connectionCache.connectionPromise) {
|
|
try {
|
|
await connectionCache.connectionPromise;
|
|
return {
|
|
ssh: connectionCache.ssh,
|
|
connection: connectionCache.dbConnection
|
|
};
|
|
} catch (error) {
|
|
// If that connection attempt failed, we'll try again below
|
|
console.error('Error waiting for existing connection:', error);
|
|
}
|
|
}
|
|
|
|
// Close existing connections if they exist
|
|
if (connectionCache.dbConnection) {
|
|
try {
|
|
await connectionCache.dbConnection.end();
|
|
} catch (error) {
|
|
console.error('Error closing existing database connection:', error);
|
|
}
|
|
}
|
|
|
|
if (connectionCache.ssh) {
|
|
try {
|
|
connectionCache.ssh.end();
|
|
} catch (error) {
|
|
console.error('Error closing existing SSH connection:', error);
|
|
}
|
|
}
|
|
|
|
// Mark that we're establishing a new connection
|
|
connectionCache.isConnecting = true;
|
|
|
|
// Create a new promise for this connection attempt
|
|
connectionCache.connectionPromise = setupSshTunnel().then(tunnel => {
|
|
const { ssh, stream, dbConfig } = tunnel;
|
|
|
|
return mysql.createConnection({
|
|
...dbConfig,
|
|
stream
|
|
}).then(connection => {
|
|
// Store the new connections
|
|
connectionCache.ssh = ssh;
|
|
connectionCache.dbConnection = connection;
|
|
connectionCache.lastUsed = Date.now();
|
|
connectionCache.isConnecting = false;
|
|
|
|
return {
|
|
ssh,
|
|
connection
|
|
};
|
|
});
|
|
}).catch(error => {
|
|
connectionCache.isConnecting = false;
|
|
throw error;
|
|
});
|
|
|
|
// Wait for the connection to be established
|
|
return connectionCache.connectionPromise;
|
|
}
|
|
|
|
/**
|
|
* Get cached query results or execute query if not cached
|
|
* @param {string} cacheKey - Unique key to identify the query
|
|
* @param {string} queryType - Type of query (field-options, product-lines, etc.)
|
|
* @param {Function} queryFn - Function to execute if cache miss
|
|
* @returns {Promise<any>} The query result
|
|
*/
|
|
async function getCachedQuery(cacheKey, queryType, queryFn) {
|
|
// Get cache duration based on query type
|
|
const cacheDuration = connectionCache.cacheDuration[queryType] || connectionCache.cacheDuration.default;
|
|
|
|
// Check if we have a valid cached result
|
|
const cachedResult = connectionCache.queryCache.get(cacheKey);
|
|
const now = Date.now();
|
|
|
|
if (cachedResult && (now - cachedResult.timestamp < cacheDuration)) {
|
|
console.log(`Cache hit for ${queryType} query: ${cacheKey}`);
|
|
return cachedResult.data;
|
|
}
|
|
|
|
// No valid cache found, execute the query
|
|
console.log(`Cache miss for ${queryType} query: ${cacheKey}`);
|
|
const result = await queryFn();
|
|
|
|
// Cache the result
|
|
connectionCache.queryCache.set(cacheKey, {
|
|
data: result,
|
|
timestamp: now
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Setup SSH tunnel to production database
|
|
* @private - Should only be used by getDbConnection
|
|
* @returns {Promise<{ssh: object, stream: object, dbConfig: object}>}
|
|
*/
|
|
async function setupSshTunnel() {
|
|
const sshConfig = {
|
|
host: process.env.PROD_SSH_HOST,
|
|
port: process.env.PROD_SSH_PORT || 22,
|
|
username: process.env.PROD_SSH_USER,
|
|
privateKey: process.env.PROD_SSH_KEY_PATH
|
|
? fs.readFileSync(process.env.PROD_SSH_KEY_PATH)
|
|
: undefined,
|
|
compress: true
|
|
};
|
|
|
|
const dbConfig = {
|
|
host: process.env.PROD_DB_HOST || 'localhost',
|
|
user: process.env.PROD_DB_USER,
|
|
password: process.env.PROD_DB_PASSWORD,
|
|
database: process.env.PROD_DB_NAME,
|
|
port: process.env.PROD_DB_PORT || 3306,
|
|
timezone: 'Z'
|
|
};
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const ssh = new Client();
|
|
|
|
ssh.on('error', (err) => {
|
|
console.error('SSH connection error:', err);
|
|
reject(err);
|
|
});
|
|
|
|
ssh.on('ready', () => {
|
|
ssh.forwardOut(
|
|
'127.0.0.1',
|
|
0,
|
|
dbConfig.host,
|
|
dbConfig.port,
|
|
(err, stream) => {
|
|
if (err) reject(err);
|
|
resolve({ ssh, stream, dbConfig });
|
|
}
|
|
);
|
|
}).connect(sshConfig);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear cached query results
|
|
* @param {string} [cacheKey] - Specific cache key to clear (clears all if not provided)
|
|
*/
|
|
function clearQueryCache(cacheKey) {
|
|
if (cacheKey) {
|
|
connectionCache.queryCache.delete(cacheKey);
|
|
console.log(`Cleared cache for key: ${cacheKey}`);
|
|
} else {
|
|
connectionCache.queryCache.clear();
|
|
console.log('Cleared all query cache');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force close all active connections
|
|
* Useful for server shutdown or manual connection reset
|
|
*/
|
|
async function closeAllConnections() {
|
|
if (connectionCache.dbConnection) {
|
|
try {
|
|
await connectionCache.dbConnection.end();
|
|
console.log('Closed database connection');
|
|
} catch (error) {
|
|
console.error('Error closing database connection:', error);
|
|
}
|
|
connectionCache.dbConnection = null;
|
|
}
|
|
|
|
if (connectionCache.ssh) {
|
|
try {
|
|
connectionCache.ssh.end();
|
|
console.log('Closed SSH connection');
|
|
} catch (error) {
|
|
console.error('Error closing SSH connection:', error);
|
|
}
|
|
connectionCache.ssh = null;
|
|
}
|
|
|
|
connectionCache.lastUsed = 0;
|
|
connectionCache.isConnecting = false;
|
|
connectionCache.connectionPromise = null;
|
|
}
|
|
|
|
module.exports = {
|
|
getDbConnection,
|
|
getCachedQuery,
|
|
clearQueryCache,
|
|
closeAllConnections
|
|
};
|