agmission/Development/server/helpers/mongo_enhanced.js

159 lines
5.4 KiB
JavaScript

'use strict';
/**
* Enhanced MongoDB transaction implementation with robust error handling
* for resolving the "session.endSession is not a function" error
*
* This file adds a drop-in replacement for the original mongo.js runInTransaction
* function that properly handles session management and transaction errors.
*/
const mongoose = require('mongoose'),
debug = require('debug')('agm:mongo_enhanced');
/**
* Transaction options with settings that reduce the likelihood of transaction aborts
*/
const DEFAULT_TRANSACTION_OPTIONS = {
readConcern: { level: 'majority' },
writeConcern: { w: 'majority' },
maxCommitTimeMS: 60000 // Increased from 10000 to 60000 (1 minute)
};
/**
* Retry configuration
*/
const MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '5', 10);
const RETRY_DELAY_MS = parseInt(process.env.RETRY_DELAY || '1000', 10);
/**
* Transaction retry helper with exponential backoff and jitter
* @param {Function} action Function to execute with retry logic
* @param {Number} maxRetries Maximum number of retries (default: 5)
* @returns {Promise<any>} Result of the action
*/
async function withTransactionRetry(action, maxRetries = MAX_RETRIES) {
let retryCount = 0;
while (true) {
try {
return await action();
} catch (error) {
retryCount++;
// Check if we should retry or just throw the error
const isTransientError = error.errorLabels &&
(error.errorLabels.includes("TransientTransactionError") ||
error.errorLabels.includes("UnknownTransactionCommitResult"));
const isTransactionAborted = error.message && (
error.message.includes("has been aborted") ||
error.message.includes("transaction aborted") ||
error.message.includes("Transaction has been aborted") ||
error.message.includes("Cannot use a session that has ended") ||
error.message.includes("Transaction") && error.message.includes("aborted")
);
const isSessionError = error.message &&
(error.message.includes("session") ||
error.message.includes("transaction"));
if ((isTransientError || isTransactionAborted || isSessionError) && retryCount <= maxRetries) {
// Calculate delay with exponential backoff and jitter
const baseDelay = RETRY_DELAY_MS * Math.pow(2, retryCount - 1);
const jitter = Math.random() * 0.5 * baseDelay; // Up to 50% jitter
const delay = baseDelay + jitter;
debug(`Retrying transaction (attempt ${retryCount}/${maxRetries}) after ${Math.round(delay)}ms delay due to error: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// Throw detailed error for better debugging
const errorDetail = {
message: error.message,
errorLabels: error.errorLabels,
retryCount,
stack: error.stack
};
debug(`Transaction failed after ${retryCount} retries: ${JSON.stringify(errorDetail, null, 2)}`);
throw error;
}
}
}
}
/**
* Safe MongoDB operation with error handling and retry
* @param {Function} operation MongoDB operation to execute
* @param {Object} session MongoDB session
* @param {Number} maxRetries Maximum number of retries (default: MAX_RETRIES)
* @returns {Promise<any>} Result of the operation
*/
async function safeMongoOperation(operation, session, maxRetries = MAX_RETRIES) {
return withTransactionRetry(async () => {
try {
return await operation(session);
} catch (error) {
// Add additional context to the error
error.operation = operation.name || 'unknown';
error.sessionValid = !!(session && typeof session.abortTransaction === 'function');
throw error;
}
}, maxRetries);
}
/**
* Enhanced version of runInTransaction that handles session management properly
* and resolves the "session.endSession is not a function" error
*
* @param {Function} func Function to run within the transaction
* @param {Object} options Transaction options (optional)
* @returns {Promise<any>} Result of the function
*/
async function enhancedRunInTransaction(func, options = null) {
if (!func || typeof (func) !== "function") {
throw new Error("invalid_func_param");
}
const tranOptions = options || DEFAULT_TRANSACTION_OPTIONS;
return withTransactionRetry(async () => {
// Always create a new session for each transaction attempt
const session = await mongoose.startSession(tranOptions);
try {
session.startTransaction();
const result = await func(session);
await session.commitTransaction();
return result;
} catch (error) {
// Safely abort the transaction
if (session && typeof session.abortTransaction === 'function' && session.inTransaction()) {
try {
await session.abortTransaction();
} catch (abortError) {
debug(`Error aborting transaction: ${abortError.message}`);
// Continue with original error
}
}
throw error;
} finally {
// Always properly end the session
if (session && typeof session.endSession === 'function') {
try {
session.endSession();
} catch (endError) {
debug(`Error ending session: ${endError.message}`);
// Don't throw this error as it's in finally block
}
}
}
});
}
module.exports = {
enhancedRunInTransaction,
withTransactionRetry,
safeMongoOperation,
DEFAULT_TRANSACTION_OPTIONS
};