pg-transactional-outbox
Version:
A PostgreSQL based transactional outbox and inbox pattern implementation to support exactly once message processing (with at least once message delivery).
108 lines • 6.55 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createErrorHandler = void 0;
const database_1 = require("../common/database");
const error_1 = require("../common/error");
const utils_1 = require("../common/utils");
const increase_message_finished_attempts_1 = require("../message/increase-message-finished-attempts");
const mark_message_abandoned_1 = require("../message/mark-message-abandoned");
const message_handler_selector_1 = require("./message-handler-selector");
/**
* Create a function to execute the error handling orchestration logic for one
* message. It executes the retry attempts logic and executes the message
* handlers `handleError` function if it exists.
* That executor function will not throw an error.
*/
const createErrorHandler = (messageHandlers, strategies, config, logger) => {
const handlerSelector = (0, message_handler_selector_1.messageHandlerSelector)(messageHandlers);
/**
* An error handler that will increase the "finished_attempts" counter.
* The message handler `handleError` is called to allow your code to handle or
* resolve the error.
* @param message: the stored message that failed to be processed
* @param error: the error that was thrown while processing the message
* @returns A boolean value if the message should be retried again in the context of the replication listener
*/
return (message, error) => __awaiter(void 0, void 0, void 0, function* () {
const handler = handlerSelector(message);
const transactionLevel = strategies.messageProcessingTransactionLevelStrategy(message);
try {
return yield (0, utils_1.executeTransaction)(yield strategies.messageProcessingDbClientStrategy.getClient(message), (client) => __awaiter(void 0, void 0, void 0, function* () {
message.finishedAttempts++;
const shouldRetry = strategies.messageRetryStrategy(message, error, 'message-handler');
if (handler === null || handler === void 0 ? void 0 : handler.handleError) {
yield handler.handleError(error, message, client, shouldRetry);
}
if (shouldRetry) {
yield (0, increase_message_finished_attempts_1.increaseMessageFinishedAttempts)(message, client, config);
Object.assign(error, { messageObject: message });
logger.warn(error, `An error ocurred while processing the ${config.outboxOrInbox} message with id ${message.id}. Retrying it again.`);
}
else {
yield (0, mark_message_abandoned_1.markMessageAbandoned)(message, client, config);
logger.error(new error_1.MessageError(error.message, 'GIVING_UP_MESSAGE_HANDLING', message, error), `Giving up processing the ${config.outboxOrInbox} message with id ${message.id}.`);
}
return shouldRetry;
}), transactionLevel);
}
catch (err) {
const msg = handler
? `The error handling of the ${config.outboxOrInbox} message failed. Please make sure that your error handling code does not throw an error!`
: `The error handling of the ${config.outboxOrInbox} message failed.`;
const error = new error_1.MessageError(msg, 'MESSAGE_ERROR_HANDLING_FAILED', message, err);
const shouldRetry = strategies.messageRetryStrategy(message, error, 'message-handler');
updateMessageText(error, shouldRetry);
logger.error(error, error.message);
// In case the error handling logic failed do a best effort to increase the "finished_attempts" counter or abandon the message
try {
yield bestEffortMessageUpdate(strategies, message, shouldRetry, config, transactionLevel);
return strategies.messageRetryStrategy(message, error, 'error-handler');
}
catch (bestEffortError) {
const e = new error_1.MessageError(`The 'best-effort' logic to increase the ${config.outboxOrInbox} message finished attempts failed as well.`, 'MESSAGE_ERROR_HANDLING_FAILED', message, bestEffortError);
const shouldRetry = strategies.messageRetryStrategy(message, e, 'error-handler-error');
updateMessageText(e, shouldRetry);
logger.error(e, e.message);
return shouldRetry;
}
}
});
};
exports.createErrorHandler = createErrorHandler;
const updateMessageText = (e, shouldRetry) => (e.message = `${e.message} Attempting to ${shouldRetry ? 'retry' : 'abandon'} the message.`);
const bestEffortMessageUpdate = (strategies, message, shouldRetry, config, transactionLevel) => __awaiter(void 0, void 0, void 0, function* () {
let i = 0;
do {
try {
yield (0, utils_1.executeTransaction)(yield strategies.messageProcessingDbClientStrategy.getClient(message), (client) => __awaiter(void 0, void 0, void 0, function* () {
if (shouldRetry) {
yield (0, increase_message_finished_attempts_1.increaseMessageFinishedAttempts)(message, client, config);
}
else {
yield (0, mark_message_abandoned_1.markMessageAbandoned)(message, client, config);
}
}), transactionLevel);
return;
}
catch (error) {
i++;
// retry for serialization failure = 40001 and deadlock detected = 40P01 PG errors
if (i < 3 && (0, database_1.isPgSerializationError)(error)) {
yield (0, utils_1.sleep)(i * 100);
}
else {
throw error;
}
}
} while (i < 3);
});
//# sourceMappingURL=create-error-handler.js.map