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).
60 lines • 3.74 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.startedAttemptsIncrement = void 0;
/**
* This function increases the "started_attempts" for the outbox or inbox
* message by one in the table. This number can then be compared to the
* "finished_attempts" number which is only increased when a message processing
* exception was correctly handled or an error was caught. A difference between
* the two can only happen if the service crashes after increasing the
* "started_attempts" but before successfully marking the message as done
* (success case) or catching an error (error case). If the "started_attempts"
* and the "finished_attempts" field differ by more than one, the chances are
* high that this message is causing a service crash.
* It sets the started_attempts, finished_attempts, locked_until. abandoned_at,
* and processed_at values on the message.
* For additional safety it makes sure, that the message was not and is not
* currently being worked on.
* @param message The message for which to acquire a lock and increment the started_attempts
* @param client The database client. Must be part of a transaction that runs before the message handling transaction.
* @param config The configuration settings that defines the database schema.
* @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 true.
*/
const startedAttemptsIncrement = (message_1, client_1, _a) => __awaiter(void 0, [message_1, client_1, _a], void 0, function* (message, client, { settings }) {
var _b;
// Use a NOWAIT select to fully lock and immediately fail if another process is locking that message row
const updateResult = yield client.query(
/* sql */ `
UPDATE ${settings.dbSchema}.${settings.dbTable} SET started_attempts = started_attempts + 1 WHERE id IN
(SELECT id FROM ${settings.dbSchema}.${settings.dbTable} WHERE id = $1 FOR UPDATE NOWAIT)
RETURNING started_attempts, finished_attempts, locked_until, processed_at, abandoned_at;`, [message.id]);
if (updateResult.rowCount === 0) {
return 'MESSAGE_NOT_FOUND';
}
const { started_attempts, finished_attempts, locked_until, processed_at, abandoned_at, } = updateResult.rows[0];
if (processed_at) {
return 'ALREADY_PROCESSED';
}
if (abandoned_at) {
return 'ABANDONED_MESSAGE';
}
// set the values for the poisonous message strategy
message.startedAttempts = started_attempts;
message.finishedAttempts = finished_attempts;
message.lockedUntil = (_b = locked_until === null || locked_until === void 0 ? void 0 : locked_until.toISOString()) !== null && _b !== void 0 ? _b : null;
// If we get here the message is neither processed nor abandoned
message.processedAt = null;
message.abandonedAt = null;
return true;
});
exports.startedAttemptsIncrement = startedAttemptsIncrement;
//# sourceMappingURL=started-attempts-increment.js.map