@remit/queue-sidecar 0.0.9 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/queue-sidecar",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
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
- `UPDATE messages SET
436
- queue_name = ?, receive_count = 0, visible_at = 0,
437
- receipt_handle = NULL, first_received_at = NULL, sent_at = ?
438
- WHERE id = ?`,
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(deadLetterName, now, message.id);
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 {