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).
81 lines • 4.88 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.initiateMessageProcessing = void 0;
const utils_1 = require("../common/utils");
/**
* This function makes sure the message was not and is not currently being
* worked on and acquires a lock to prevent other processes to work with this
* message. It locks the record via `SELECT ... FOR NO KEY UPDATE`. It updates
* the message object and sets the started_attempts, finished_attempts,
* locked_until, and processed_at values (again) on the message to be sure no
* other process altered them.
* @param message The message for which to acquire a lock and set the updatable properties (again).
* @param client The database client. Must be part of the transaction where the message handling changes are later done.
* @param config The configuration settings for the polling or replication listener.
* @param messageNotFoundRetryStrategy The retry strategy if the message could not be found in the database.
* @returns 'MESSAGE_NOT_FOUND' if the message was not found, 'ALREADY_PROCESSED' if it was processed, and otherwise assigns the properties to the message and returns it.
*/
const initiateMessageProcessing = (message, client, settings, messageNotFoundRetryStrategy) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
const selectResult = yield loadAndLockMessage(message, client, settings, messageNotFoundRetryStrategy);
if (selectResult === 'MESSAGE_NOT_FOUND') {
return 'MESSAGE_NOT_FOUND';
}
const { started_attempts, finished_attempts, processed_at, abandoned_at, locked_until, } = selectResult.rows[0];
// ensures latest values (e.g. if the `startedAttemptsIncrement` was not called or another
// process changed them between the `startedAttemptsIncrement` and this call.
message.startedAttempts = started_attempts;
message.finishedAttempts = finished_attempts;
message.lockedUntil = locked_until === null || locked_until === void 0 ? void 0 : locked_until.toISOString();
message.processedAt = (_a = processed_at === null || processed_at === void 0 ? void 0 : processed_at.toISOString()) !== null && _a !== void 0 ? _a : null;
message.abandonedAt = (_b = abandoned_at === null || abandoned_at === void 0 ? void 0 : abandoned_at.toISOString()) !== null && _b !== void 0 ? _b : null;
if (processed_at) {
return 'ALREADY_PROCESSED';
}
if (abandoned_at) {
return 'ABANDONED_MESSAGE';
}
return true;
});
exports.initiateMessageProcessing = initiateMessageProcessing;
/**
* Load and lock (and potentially retry) a message.
* @param message The message for which to acquire the lock.
* @param client The database client. Must be part of the transaction where the message handling changes are later done.
* @param config The configuration settings for the polling or replication listener.
* @param messageNotFoundRetryStrategy The retry strategy if the message could not be found in the database.
* @returns 'MESSAGE_NOT_FOUND' if the message was not found, otherwise the message details from the database.
*/
const loadAndLockMessage = (message, client, settings, messageNotFoundRetryStrategy) => __awaiter(void 0, void 0, void 0, function* () {
let selectResult;
let attempts = 0;
do {
// Use a NOWAIT select to immediately fail if another process is locking that message
selectResult = yield client.query(
/* sql */ `
SELECT started_attempts, finished_attempts, processed_at, abandoned_at, locked_until FROM ${settings.dbSchema}.${settings.dbTable} WHERE id = $1 FOR NO KEY UPDATE NOWAIT;`, [message.id]);
if (selectResult.rowCount === 0 || selectResult.rowCount === null) {
const { retry, delayInMs } = messageNotFoundRetryStrategy(message, ++attempts);
if (retry) {
yield (0, utils_1.sleep)(delayInMs);
}
else {
return 'MESSAGE_NOT_FOUND';
}
}
else {
return selectResult;
}
// eslint-disable-next-line no-constant-condition
} while (true);
});
//# sourceMappingURL=initiate-message-processing.js.map