@remit/queue-sidecar 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/store.test.ts +54 -0
- package/src/store.ts +60 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/queue-sidecar",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "node --import tsx src/main.ts",
|
|
18
18
|
"test:typecheck": "tsgo --noEmit",
|
|
19
|
-
"test:run": "node --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=93 --test 'src/**/*.test.ts'",
|
|
19
|
+
"test:run": "node $NODE_TEST_FLAGS --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=93 --test 'src/**/*.test.ts'",
|
|
20
20
|
"test": "npm run test:typecheck && npm run test:run"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
package/src/store.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { after, beforeEach, describe, it } from "node:test";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
6
7
|
import { bootstrapQueues, parseQueuesConfig } from "./queues-config.js";
|
|
7
8
|
import {
|
|
8
9
|
InvalidParameterValueError,
|
|
@@ -317,4 +318,57 @@ describe("QueueStore", () => {
|
|
|
317
318
|
0,
|
|
318
319
|
);
|
|
319
320
|
});
|
|
321
|
+
|
|
322
|
+
const seedBacklog = (rows: number, groups: number): void => {
|
|
323
|
+
const path = join(dir, "queue.db");
|
|
324
|
+
store.close();
|
|
325
|
+
const seed = new Database(path);
|
|
326
|
+
seed
|
|
327
|
+
.prepare(
|
|
328
|
+
`WITH RECURSIVE n(i) AS (
|
|
329
|
+
SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < ?
|
|
330
|
+
)
|
|
331
|
+
INSERT INTO messages (
|
|
332
|
+
message_id, queue_name, body, md5_body, group_id, dedup_id,
|
|
333
|
+
sequence_number, receive_count, visible_at, receipt_handle,
|
|
334
|
+
sent_at, first_received_at
|
|
335
|
+
)
|
|
336
|
+
SELECT 'm' || i, 'orders.fifo', 'x', 'x', 'g' || CAST(i % CAST(? AS INTEGER) AS INTEGER), NULL,
|
|
337
|
+
i, 0, 0, NULL, 0, NULL
|
|
338
|
+
FROM n`,
|
|
339
|
+
)
|
|
340
|
+
.run(rows, groups);
|
|
341
|
+
seed.close();
|
|
342
|
+
store = new QueueStore(path);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
it("receives only the head of each group from a FIFO backlog", () => {
|
|
346
|
+
seedBacklog(30, 3);
|
|
347
|
+
const received = store.receiveMessages({
|
|
348
|
+
queueName: "orders.fifo",
|
|
349
|
+
maxMessages: 10,
|
|
350
|
+
now: 1_000,
|
|
351
|
+
});
|
|
352
|
+
assert.deepEqual(
|
|
353
|
+
received.map((m) => [m.messageId, m.groupId]),
|
|
354
|
+
[
|
|
355
|
+
["m1", "g1"],
|
|
356
|
+
["m2", "g2"],
|
|
357
|
+
["m3", "g0"],
|
|
358
|
+
],
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("receives from a 200k-row FIFO backlog within a fixed time budget", () => {
|
|
363
|
+
seedBacklog(200_000, 5);
|
|
364
|
+
const started = performance.now();
|
|
365
|
+
const received = store.receiveMessages({
|
|
366
|
+
queueName: "orders.fifo",
|
|
367
|
+
maxMessages: 10,
|
|
368
|
+
now: 1_000,
|
|
369
|
+
});
|
|
370
|
+
const elapsed = performance.now() - started;
|
|
371
|
+
assert.equal(received.length, 5);
|
|
372
|
+
assert.ok(elapsed < 500, `receive took ${elapsed.toFixed(1)} ms`);
|
|
373
|
+
});
|
|
320
374
|
});
|
package/src/store.ts
CHANGED
|
@@ -139,6 +139,8 @@ export class QueueStore {
|
|
|
139
139
|
);
|
|
140
140
|
CREATE INDEX IF NOT EXISTS messages_queue_order
|
|
141
141
|
ON messages (queue_name, id);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS messages_queue_group
|
|
143
|
+
ON messages (queue_name, group_id, id);
|
|
142
144
|
CREATE TABLE IF NOT EXISTS dedup (
|
|
143
145
|
queue_name TEXT NOT NULL,
|
|
144
146
|
dedup_id TEXT NOT NULL,
|
|
@@ -305,32 +307,13 @@ export class QueueStore {
|
|
|
305
307
|
input.visibilityTimeoutSeconds ?? queue.visibilityTimeoutSeconds;
|
|
306
308
|
|
|
307
309
|
const receive = this.db.transaction((): ReceivedMessage[] => {
|
|
308
|
-
const inFlightGroups = new Set(
|
|
309
|
-
(
|
|
310
|
-
this.db
|
|
311
|
-
.prepare(
|
|
312
|
-
`SELECT DISTINCT group_id FROM messages
|
|
313
|
-
WHERE queue_name = ? AND group_id IS NOT NULL
|
|
314
|
-
AND visible_at > ?`,
|
|
315
|
-
)
|
|
316
|
-
.all(input.queueName, now) as { group_id: string }[]
|
|
317
|
-
).map((r) => r.group_id),
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
const candidates = this.db
|
|
321
|
-
.prepare(
|
|
322
|
-
`SELECT * FROM messages
|
|
323
|
-
WHERE queue_name = ? AND visible_at <= ?
|
|
324
|
-
ORDER BY id ASC`,
|
|
325
|
-
)
|
|
326
|
-
.all(input.queueName, now) as MessageRow[];
|
|
327
|
-
|
|
328
310
|
const picked: ReceivedMessage[] = [];
|
|
329
|
-
const claimedGroups = new Set<string>();
|
|
330
|
-
|
|
331
|
-
for (const candidate of candidates) {
|
|
332
|
-
if (picked.length >= input.maxMessages) break;
|
|
333
311
|
|
|
312
|
+
for (const candidate of this.nextCandidates(
|
|
313
|
+
queue,
|
|
314
|
+
now,
|
|
315
|
+
input.maxMessages,
|
|
316
|
+
)) {
|
|
334
317
|
if (
|
|
335
318
|
queue.maxReceiveCount !== null &&
|
|
336
319
|
queue.deadLetterTargetName !== null &&
|
|
@@ -340,12 +323,6 @@ export class QueueStore {
|
|
|
340
323
|
continue;
|
|
341
324
|
}
|
|
342
325
|
|
|
343
|
-
if (queue.fifo && candidate.group_id !== null) {
|
|
344
|
-
if (inFlightGroups.has(candidate.group_id)) continue;
|
|
345
|
-
if (claimedGroups.has(candidate.group_id)) continue;
|
|
346
|
-
claimedGroups.add(candidate.group_id);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
326
|
const receiptHandle = randomUUID();
|
|
350
327
|
const receiveCount = candidate.receive_count + 1;
|
|
351
328
|
const firstReceived = candidate.first_received_at ?? now;
|
|
@@ -425,19 +402,67 @@ export class QueueStore {
|
|
|
425
402
|
};
|
|
426
403
|
}
|
|
427
404
|
|
|
405
|
+
private nextCandidates(
|
|
406
|
+
queue: QueueRecord,
|
|
407
|
+
now: number,
|
|
408
|
+
limit: number,
|
|
409
|
+
): MessageRow[] {
|
|
410
|
+
if (!queue.fifo) {
|
|
411
|
+
return this.db
|
|
412
|
+
.prepare(
|
|
413
|
+
`SELECT * FROM messages
|
|
414
|
+
WHERE queue_name = ? AND visible_at <= ?
|
|
415
|
+
ORDER BY id ASC LIMIT ?`,
|
|
416
|
+
)
|
|
417
|
+
.all(queue.name, now, limit) as MessageRow[];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return this.db
|
|
421
|
+
.prepare(
|
|
422
|
+
`WITH RECURSIVE g(gid) AS (
|
|
423
|
+
SELECT MIN(group_id) FROM messages WHERE queue_name = @queue
|
|
424
|
+
UNION ALL
|
|
425
|
+
SELECT (
|
|
426
|
+
SELECT MIN(group_id) FROM messages
|
|
427
|
+
WHERE queue_name = @queue AND group_id > g.gid
|
|
428
|
+
)
|
|
429
|
+
FROM g WHERE g.gid IS NOT NULL
|
|
430
|
+
)
|
|
431
|
+
SELECT m.* FROM g
|
|
432
|
+
JOIN messages m ON m.id = (
|
|
433
|
+
SELECT MIN(id) FROM messages
|
|
434
|
+
WHERE queue_name = @queue AND group_id = g.gid
|
|
435
|
+
)
|
|
436
|
+
WHERE m.visible_at <= @now
|
|
437
|
+
ORDER BY m.id ASC LIMIT @limit`,
|
|
438
|
+
)
|
|
439
|
+
.all({ queue: queue.name, now, limit }) as MessageRow[];
|
|
440
|
+
}
|
|
441
|
+
|
|
428
442
|
private moveToDeadLetter(
|
|
429
443
|
message: MessageRow,
|
|
430
444
|
deadLetterName: string,
|
|
431
445
|
now: number,
|
|
432
446
|
): void {
|
|
447
|
+
this.db.prepare("DELETE FROM messages WHERE id = ?").run(message.id);
|
|
433
448
|
this.db
|
|
434
449
|
.prepare(
|
|
435
|
-
`
|
|
436
|
-
queue_name
|
|
437
|
-
|
|
438
|
-
|
|
450
|
+
`INSERT INTO messages (
|
|
451
|
+
message_id, queue_name, body, md5_body, group_id, dedup_id,
|
|
452
|
+
sequence_number, receive_count, visible_at, receipt_handle,
|
|
453
|
+
sent_at, first_received_at
|
|
454
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?, NULL)`,
|
|
439
455
|
)
|
|
440
|
-
.run(
|
|
456
|
+
.run(
|
|
457
|
+
message.message_id,
|
|
458
|
+
deadLetterName,
|
|
459
|
+
message.body,
|
|
460
|
+
message.md5_body,
|
|
461
|
+
message.group_id,
|
|
462
|
+
message.dedup_id,
|
|
463
|
+
message.sequence_number,
|
|
464
|
+
now,
|
|
465
|
+
);
|
|
441
466
|
}
|
|
442
467
|
|
|
443
468
|
private pruneDedup(queueName: string, now: number): void {
|