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
JavaScript
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();
}
}