UNPKG

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).

117 lines 7.12 kB
"use strict"; 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.createMessageHandler = void 0; const database_1 = require("../common/database"); const error_1 = require("../common/error"); const utils_1 = require("../common/utils"); const initiate_message_processing_1 = require("../message/initiate-message-processing"); const mark_message_abandoned_1 = require("../message/mark-message-abandoned"); const mark_message_completed_1 = require("../message/mark-message-completed"); const started_attempts_increment_1 = require("../message/started-attempts-increment"); const message_handler_selector_1 = require("./message-handler-selector"); /** * Executes the message verification and poisonous message verification in one * transaction (if enabled) and the actual message handler and marking the * message as processed in another transaction. */ const createMessageHandler = (messageHandlers, strategies, config, logger, listenerType) => { const handlerSelector = (0, message_handler_selector_1.messageHandlerSelector)(messageHandlers); return (message, cancellation) => __awaiter(void 0, void 0, void 0, function* () { const handler = handlerSelector(message); if (!handler) { logger.debug(`No ${config.outboxOrInbox} message handler found for aggregate type "${message.aggregateType}" and message tye "${message.messageType}"`); } if (handler && config.settings.enablePoisonousMessageProtection !== false) { if (listenerType === 'replication') { const continueProcessing = yield applyReplicationPoisonousMessageProtection(message, strategies, config, logger); if (!continueProcessing) { return; } } // The startedAttempts was incremented in `startedAttemptsIncrement` or from the polling function // so the difference is always at least one const diff = message.startedAttempts - message.finishedAttempts; if (diff >= 2) { const retry = strategies.poisonousMessageRetryStrategy(message); if (!retry) { const msg = `Stopped processing the ${config.outboxOrInbox} message with ID ${message.id} as it is likely a poisonous message.`; logger.error(new error_1.TransactionalOutboxInboxError(msg, 'POISONOUS_MESSAGE'), msg); yield abandonPoisonousMessage(message, strategies, config); return; } } } yield processMessage(message, handler, strategies, cancellation, config, logger); }); }; exports.createMessageHandler = createMessageHandler; /** * When using the logical replication approach this function tries to increment * the started attempts of the message. This can then be compared to the * finished attempts to decide if the message should be retried. */ const applyReplicationPoisonousMessageProtection = (message, strategies, config, logger) => __awaiter(void 0, void 0, void 0, function* () { const transactionLevel = strategies.messageProcessingTransactionLevelStrategy(message); return yield (0, utils_1.executeTransaction)(yield strategies.messageProcessingDbClientStrategy.getClient(message), (client) => __awaiter(void 0, void 0, void 0, function* () { // Increment the started_attempts const result = yield (0, started_attempts_increment_1.startedAttemptsIncrement)(message, client, config); if (result !== true) { logger.warn(message, `Could not increment the started attempts field of the received ${config.outboxOrInbox} message: ${result}`); yield client.query('ROLLBACK'); // don't increment the start attempts again on a processed message return false; } return true; }), transactionLevel); }); /** * Mark the message as abandoned if it is found to be (likely) a poisonous message */ const abandonPoisonousMessage = (message, strategies, config) => __awaiter(void 0, void 0, void 0, function* () { const transactionLevel = strategies.messageProcessingTransactionLevelStrategy(message); return yield (0, utils_1.executeTransaction)(yield strategies.messageProcessingDbClientStrategy.getClient(message), (client) => __awaiter(void 0, void 0, void 0, function* () { yield (0, mark_message_abandoned_1.markMessageAbandoned)(message, client, config); }), transactionLevel); }); /** Lock the message and execute the handler (if there is any) and mark the message as completed */ const processMessage = (message, handler, strategies, cancellation, config, logger) => __awaiter(void 0, void 0, void 0, function* () { const transactionLevel = strategies.messageProcessingTransactionLevelStrategy(message); yield (0, utils_1.executeTransaction)(yield strategies.messageProcessingDbClientStrategy.getClient(message), (client) => __awaiter(void 0, void 0, void 0, function* () { let timedOut = false; if (handler) { cancellation.on('timeout', () => __awaiter(void 0, void 0, void 0, function* () { timedOut = true; // roll back the current changes and release/end the client to disable further changes yield client.query('ROLLBACK'); if ('release' in client) { client.release(new error_1.TransactionalOutboxInboxError('Message processing timeout', 'TIMEOUT')); } else if ('end' in client) { yield client.end(); } yield (0, utils_1.justDoIt)(() => { (0, database_1.releaseIfPoolClient)(client); }); })); // lock the message from further processing const result = yield (0, initiate_message_processing_1.initiateMessageProcessing)(message, client, config.settings, strategies.messageNotFoundRetryStrategy); if (result !== true) { logger.warn(message, `The received ${config.outboxOrInbox} message cannot be processed: ${result}`); return; } yield handler.handle(message, client); } if (!timedOut) { yield (0, mark_message_completed_1.markMessageCompleted)(message, client, config); } }), transactionLevel); }); //# sourceMappingURL=create-message-handler.js.map