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