UNPKG

mcp-postgres-full-access

Version:

Full-access PostgreSQL server for Model Context Protocol with read/write capabilities and enhanced schema metadata

161 lines (160 loc) 6.14 kB
import { safelyReleaseClient } from "./utils.js"; export class TransactionManager { activeTransactions = new Map(); monitorInterval = null; transactionTimeoutMs; monitorIntervalMs; monitorEnabled; constructor(transactionTimeoutMs = 15000, monitorIntervalMs = 5000, monitorEnabled = true) { this.transactionTimeoutMs = transactionTimeoutMs; this.monitorIntervalMs = monitorIntervalMs; this.monitorEnabled = monitorEnabled; } /** * Add a new transaction to the manager */ addTransaction(id, client, sql) { this.activeTransactions.set(id, { id, client, startTime: Date.now(), sql: sql.substring(0, 100), // Store beginning of query for debugging state: 'active', released: false }); } /** * Get a transaction by ID */ getTransaction(id) { return this.activeTransactions.get(id); } /** * Remove a transaction from the manager */ removeTransaction(id) { return this.activeTransactions.delete(id); } /** * Check if a transaction exists */ hasTransaction(id) { return this.activeTransactions.has(id); } /** * Get count of active transactions */ get transactionCount() { return this.activeTransactions.size; } /** * Start the transaction monitor */ startMonitor() { if (this.monitorEnabled && !this.monitorInterval) { console.error(`Starting transaction monitor with timeout ${this.transactionTimeoutMs}ms, checking every ${this.monitorIntervalMs}ms`); this.monitorInterval = setInterval(() => this.checkStuckTransactions(), this.monitorIntervalMs); } else if (!this.monitorEnabled) { console.error("Transaction monitor is disabled"); } } /** * Stop the transaction monitor */ stopMonitor() { if (this.monitorInterval) { clearInterval(this.monitorInterval); this.monitorInterval = null; } } /** * Monitor for stuck transactions and roll them back */ checkStuckTransactions() { const now = Date.now(); let terminatedCount = 0; for (const [id, transaction] of this.activeTransactions.entries()) { // Skip already released transactions awaiting cleanup if (transaction.released) continue; const age = now - transaction.startTime; if (age > this.transactionTimeoutMs && transaction.state === 'active') { console.error(`Transaction ${id} has been running for ${age}ms and will be rolled back`); transaction.state = 'terminating'; terminatedCount++; // Handle in async function to avoid blocking the monitor (async () => { try { // Attempt rollback await transaction.client.query("ROLLBACK"); console.error(`Successfully rolled back stuck transaction ${id}`); } catch (error) { console.error(`Error rolling back transaction ${id}:`, error); } finally { // Mark as released before actually releasing to prevent double-release if (!transaction.released) { transaction.released = true; safelyReleaseClient(transaction.client); } this.removeTransaction(id); } })().catch(err => { console.error(`Unhandled error in transaction cleanup for ${id}:`, err); // Ensure cleanup even on error if (!transaction.released) { transaction.released = true; try { safelyReleaseClient(transaction.client); } catch (releaseErr) { console.error(`Final release attempt failed for ${id}:`, releaseErr); } } this.removeTransaction(id); }); } } if (terminatedCount > 0) { console.error(`Terminated ${terminatedCount} stuck transactions. Remaining active: ${this.transactionCount}`); } } /** * Clean up any pending transactions */ async cleanupTransactions() { console.error(`Cleaning up ${this.transactionCount} active transactions`); const transactionEntries = Array.from(this.activeTransactions.entries()); for (const [id, transaction] of transactionEntries) { // Skip already released transactions if (transaction.released) { console.error(`Transaction ${id} already marked as released, skipping cleanup`); this.removeTransaction(id); continue; } try { await transaction.client.query("ROLLBACK"); console.error(`Rolled back transaction ${id}`); // Mark as released to prevent double-release attempts transaction.released = true; safelyReleaseClient(transaction.client); this.removeTransaction(id); } catch (error) { console.error(`Error rolling back transaction ${id}:`, error); // Even on error, mark as released and attempt to release transaction.released = true; try { safelyReleaseClient(transaction.client); } catch (releaseErr) { console.error(`Final client release failed for ${id}:`, releaseErr); } this.removeTransaction(id); } } this.activeTransactions.clear(); } }