@arturwojnar/hermes-postgresql
Version:
Production-Ready TypeScript Outbox Pattern for PostgreSQL
170 lines • 7.68 kB
JavaScript
import { Duration } from '@arturwojnar/hermes';
import { describe, expect, jest, test } from '@jest/globals';
import { setTimeout } from 'node:timers/promises';
import { createNonBlockingPublishingQueue } from './createNonBlockingPublishingQueue.js';
describe(`createNonBlockingPublishingQueue`, () => {
const createMockTransaction = (lsn) => ({
transactionId: 1,
lsn,
timestamp: new Date(),
results: [],
});
const createMockMessage = (lsn) => ({
transaction: createMockTransaction(lsn),
acknowledge: jest.fn().mockResolvedValue(),
});
const createMockPublish = () => jest.fn().mockImplementation(async () => { });
test('should queue unique messages', async () => {
const mockPublish = createMockPublish();
const queue = createNonBlockingPublishingQueue(mockPublish, { waitAfterFailedPublish: Duration.ofSeconds(0) });
const message1 = createMockMessage('0/1');
const message2 = createMockMessage('0/2');
queue.queue(message1);
queue.queue(message2);
expect(queue.size()).toBe(2);
queue.dispose();
});
test('should ignore duplicate messages based on LSN', () => {
const mockPublish = createMockPublish();
const queue = createNonBlockingPublishingQueue(mockPublish, { waitAfterFailedPublish: Duration.ofSeconds(0) });
const message1 = createMockMessage('0/1');
const message2 = createMockMessage('0/1');
queue.queue(message1);
queue.queue(message2);
expect(queue.size()).toBe(1);
queue.dispose();
});
test('should publish messages in FIFO order if proccessing time of each message is the same', async () => {
const publishedMessages = [];
const mockPublish = jest
.fn()
.mockImplementation(async (message) => {
publishedMessages.push(message.transaction.lsn);
});
const { queue, size, run, dispose } = createNonBlockingPublishingQueue(mockPublish, {
waitAfterFailedPublish: Duration.ofSeconds(0),
});
const message1 = createMockMessage('0/1');
const message2 = createMockMessage('0/2');
const message3 = createMockMessage('0/3');
const message4 = createMockMessage('0/4');
await Promise.all([run(queue(message1)), run(queue(message2)), run(queue(message3)), run(queue(message4))]);
expect(publishedMessages).toEqual(['0/1', '0/2', '0/3', '0/4']);
expect(message1.acknowledge).toHaveBeenCalledTimes(1);
expect(message2.acknowledge).toHaveBeenCalledTimes(1);
expect(message3.acknowledge).toHaveBeenCalledTimes(1);
expect(message4.acknowledge).toHaveBeenCalledTimes(1);
expect(size()).toBe(0);
dispose();
});
test(`must confirm messages in the order despite the fact the first message's processing got delayed.`, async () => {
const timestamps = {};
const createMockMessage = (lsn) => ({
transaction: createMockTransaction(lsn),
acknowledge: jest.fn().mockImplementation(() => {
timestamps[lsn] = Date.now();
return Promise.resolve();
}),
});
const publishedMessages = [];
const mockPublish = jest
.fn()
.mockImplementationOnce(async (message) => {
await setTimeout(1000);
publishedMessages.push(message.transaction.lsn);
})
.mockImplementation(async (message) => {
publishedMessages.push(message.transaction.lsn);
});
const { queue, size, run, waitUntilIsEmpty, dispose } = createNonBlockingPublishingQueue(mockPublish, {
waitAfterFailedPublish: Duration.ofSeconds(0),
});
const message1 = createMockMessage('0/1');
const message2 = createMockMessage('0/2');
const message3 = createMockMessage('0/3');
const message4 = createMockMessage('0/4');
const message5 = createMockMessage('0/5');
const message6 = createMockMessage('0/6');
await Promise.all([run(queue(message1)), run(queue(message2)), run(queue(message3)), run(queue(message4))]);
await waitUntilIsEmpty();
queue(message5);
await run();
queue(message6);
await run();
expect(publishedMessages).toEqual(['0/2', '0/3', '0/4', '0/1', '0/5', '0/6']);
expect(message1.acknowledge).toHaveBeenCalledTimes(1);
expect(message2.acknowledge).toHaveBeenCalledTimes(1);
expect(message3.acknowledge).toHaveBeenCalledTimes(1);
expect(message4.acknowledge).toHaveBeenCalledTimes(1);
expect(message5.acknowledge).toHaveBeenCalledTimes(1);
expect(message6.acknowledge).toHaveBeenCalledTimes(1);
expect(size()).toBe(0);
const messages = [message1, message2, message3, message4, message5, message6];
messages.reduce((m1, m2) => {
expect(timestamps[m1.transaction.lsn]).toBeLessThanOrEqual(timestamps[m2.transaction.lsn]);
return m2;
});
dispose();
});
test(`when 'waitAfterFailedPublish' is set to non-zero or unspecified, then the queue must resend failed messages`, async () => {
const timestamps = {};
const createMockMessage = (lsn) => ({
transaction: createMockTransaction(lsn),
acknowledge: jest.fn().mockImplementation(() => {
timestamps[lsn] = Date.now();
return Promise.resolve();
}),
});
const publishedMessages = [];
let fails1 = 0;
let fails2 = 0;
let fails3 = 0;
const mockPublish = jest
.fn()
.mockImplementation(async (message) => {
if (message.transaction.lsn === '0/1' && fails1 < 2) {
fails1++;
return Promise.reject(new Error());
}
if (message.transaction.lsn === '0/10' && fails2 < 1) {
fails2++;
return Promise.reject(new Error());
}
if (message.transaction.lsn === '0/12' && fails2 < 1) {
publishedMessages.push(message.transaction.lsn);
await setTimeout(250);
return Promise.resolve();
}
if (message.transaction.lsn === '0/30' && fails3 < 1) {
fails3++;
return Promise.reject(new Error());
}
publishedMessages.push(message.transaction.lsn);
return Promise.resolve();
});
const { queue, size, run, waitUntilIsEmpty, dispose } = createNonBlockingPublishingQueue(mockPublish, {
waitAfterFailedPublish: Duration.ofMiliseconds(500),
});
const messages = Array(30)
.fill(0)
.map((_, i) => createMockMessage(`0/${i + 1}`));
await Promise.all(messages.map((message) => run(queue(message))));
await waitUntilIsEmpty();
messages.forEach((message) => expect(message.acknowledge).toHaveBeenCalledTimes(1));
const expectedOrder = [
...messages
.filter(({ transaction: { lsn } }) => !['0/1', '0/10', '0/30'].includes(lsn))
.map(({ transaction: { lsn } }) => lsn),
'0/1',
'0/10',
'0/30',
];
expect(publishedMessages).toEqual(expectedOrder);
expect(size()).toBe(0);
expect(fails1).toBe(2);
expect(fails2).toBe(1);
expect(fails3).toBe(1);
dispose();
});
});
//# sourceMappingURL=createNonBlockingPublishingQueue.unit.test.js.map