@remit/queue-sidecar 0.0.14 → 0.0.15
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 +4 -4
- package/src/store.test.ts +8 -3
- package/src/store.ts +242 -194
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/queue-sidecar",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@remit/logger-lambda": "*",
|
|
24
|
-
"
|
|
25
|
-
"@
|
|
26
|
-
"
|
|
24
|
+
"prom-client": "^15.1.3",
|
|
25
|
+
"@remit/drizzle-service": "*",
|
|
26
|
+
"drizzle-orm": "1.0.0-rc.5"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@aws-sdk/client-sqs": "*",
|
package/src/store.test.ts
CHANGED
|
@@ -3,7 +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
|
|
6
|
+
import { openSqlite } from "@remit/drizzle-service/sqlite";
|
|
7
7
|
import { bootstrapQueues, parseQueuesConfig } from "./queues-config.js";
|
|
8
8
|
import {
|
|
9
9
|
InvalidParameterValueError,
|
|
@@ -322,8 +322,13 @@ describe("QueueStore", () => {
|
|
|
322
322
|
const seedBacklog = (rows: number, groups: number): void => {
|
|
323
323
|
const path = join(dir, "queue.db");
|
|
324
324
|
store.close();
|
|
325
|
-
const seed =
|
|
326
|
-
|
|
325
|
+
const seed = openSqlite({
|
|
326
|
+
filename: path,
|
|
327
|
+
mode: "open",
|
|
328
|
+
synchronous: "FULL",
|
|
329
|
+
schema: {},
|
|
330
|
+
});
|
|
331
|
+
seed.sqlite
|
|
327
332
|
.prepare(
|
|
328
333
|
`WITH RECURSIVE n(i) AS (
|
|
329
334
|
SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < ?
|
package/src/store.ts
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
2
|
+
import { openSqlite, type SqliteHandle } from "@remit/drizzle-service/sqlite";
|
|
3
|
+
import { and, asc, count, eq, gt, lt, lte, sql } from "drizzle-orm";
|
|
4
|
+
import {
|
|
5
|
+
integer,
|
|
6
|
+
primaryKey,
|
|
7
|
+
sqliteTable,
|
|
8
|
+
text,
|
|
9
|
+
} from "drizzle-orm/sqlite-core";
|
|
10
|
+
|
|
11
|
+
const queues = sqliteTable("queues", {
|
|
12
|
+
name: text("name").primaryKey(),
|
|
13
|
+
fifo: integer("fifo").notNull().default(0),
|
|
14
|
+
visibilityTimeoutSeconds: integer("visibility_timeout_seconds")
|
|
15
|
+
.notNull()
|
|
16
|
+
.default(30),
|
|
17
|
+
deadLetterTargetName: text("dead_letter_target_name"),
|
|
18
|
+
maxReceiveCount: integer("max_receive_count"),
|
|
19
|
+
contentBasedDeduplication: integer("content_based_deduplication")
|
|
20
|
+
.notNull()
|
|
21
|
+
.default(0),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const messages = sqliteTable("messages", {
|
|
25
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
26
|
+
messageId: text("message_id").notNull(),
|
|
27
|
+
queueName: text("queue_name").notNull(),
|
|
28
|
+
body: text("body").notNull(),
|
|
29
|
+
md5Body: text("md5_body").notNull(),
|
|
30
|
+
groupId: text("group_id"),
|
|
31
|
+
dedupId: text("dedup_id"),
|
|
32
|
+
sequenceNumber: text("sequence_number"),
|
|
33
|
+
receiveCount: integer("receive_count").notNull().default(0),
|
|
34
|
+
visibleAt: integer("visible_at").notNull().default(0),
|
|
35
|
+
receiptHandle: text("receipt_handle"),
|
|
36
|
+
sentAt: integer("sent_at").notNull(),
|
|
37
|
+
firstReceivedAt: integer("first_received_at"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const dedup = sqliteTable(
|
|
41
|
+
"dedup",
|
|
42
|
+
{
|
|
43
|
+
queueName: text("queue_name").notNull(),
|
|
44
|
+
dedupId: text("dedup_id").notNull(),
|
|
45
|
+
messageId: text("message_id").notNull(),
|
|
46
|
+
createdAt: integer("created_at").notNull(),
|
|
47
|
+
},
|
|
48
|
+
(table) => [primaryKey({ columns: [table.queueName, table.dedupId] })],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const releaseRedrive = sqliteTable("release_redrive", {
|
|
52
|
+
id: integer("id").primaryKey(),
|
|
53
|
+
release: text("release").notNull(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const schema = { queues, messages, dedup, releaseRedrive };
|
|
3
57
|
|
|
4
58
|
export interface QueueDefinition {
|
|
5
59
|
readonly name: string;
|
|
@@ -46,43 +100,24 @@ export interface QueueAttributes {
|
|
|
46
100
|
readonly redrivePolicy: string | null;
|
|
47
101
|
}
|
|
48
102
|
|
|
49
|
-
|
|
50
|
-
name: string;
|
|
51
|
-
fifo: number;
|
|
52
|
-
visibility_timeout_seconds: number;
|
|
53
|
-
dead_letter_target_name: string | null;
|
|
54
|
-
max_receive_count: number | null;
|
|
55
|
-
content_based_deduplication: number;
|
|
56
|
-
}
|
|
103
|
+
type QueueRow = typeof queues.$inferSelect;
|
|
57
104
|
|
|
58
|
-
|
|
59
|
-
id: number;
|
|
60
|
-
message_id: string;
|
|
61
|
-
queue_name: string;
|
|
62
|
-
body: string;
|
|
63
|
-
md5_body: string;
|
|
64
|
-
group_id: string | null;
|
|
65
|
-
dedup_id: string | null;
|
|
66
|
-
sequence_number: string | null;
|
|
67
|
-
receive_count: number;
|
|
68
|
-
visible_at: number;
|
|
69
|
-
receipt_handle: string | null;
|
|
70
|
-
sent_at: number;
|
|
71
|
-
first_received_at: number | null;
|
|
72
|
-
}
|
|
105
|
+
type MessageRow = typeof messages.$inferSelect;
|
|
73
106
|
|
|
74
107
|
const DEDUP_WINDOW_MS = 5 * 60 * 1000;
|
|
75
108
|
|
|
109
|
+
const IMMEDIATE = { behavior: "immediate" } as const;
|
|
110
|
+
|
|
76
111
|
const md5Hex = (value: string): string =>
|
|
77
112
|
createHash("md5").update(value, "utf8").digest("hex");
|
|
78
113
|
|
|
79
114
|
const toQueueRecord = (row: QueueRow): QueueRecord => ({
|
|
80
115
|
name: row.name,
|
|
81
116
|
fifo: row.fifo === 1,
|
|
82
|
-
visibilityTimeoutSeconds: row.
|
|
83
|
-
deadLetterTargetName: row.
|
|
84
|
-
maxReceiveCount: row.
|
|
85
|
-
contentBasedDeduplication: row.
|
|
117
|
+
visibilityTimeoutSeconds: row.visibilityTimeoutSeconds,
|
|
118
|
+
deadLetterTargetName: row.deadLetterTargetName,
|
|
119
|
+
maxReceiveCount: row.maxReceiveCount,
|
|
120
|
+
contentBasedDeduplication: row.contentBasedDeduplication === 1,
|
|
86
121
|
});
|
|
87
122
|
|
|
88
123
|
/**
|
|
@@ -95,27 +130,31 @@ const toQueueRecord = (row: QueueRow): QueueRecord => ({
|
|
|
95
130
|
* here so the behaviour is testable without a running HTTP server.
|
|
96
131
|
*/
|
|
97
132
|
export class QueueStore {
|
|
98
|
-
private readonly
|
|
133
|
+
private readonly handle: SqliteHandle<typeof schema>;
|
|
134
|
+
private readonly db: SqliteHandle<typeof schema>["db"];
|
|
99
135
|
private sequenceCounter: bigint;
|
|
100
136
|
private readonly startedAt = Date.now();
|
|
101
137
|
private readonly lastReceiveAt = new Map<string, number>();
|
|
102
138
|
|
|
103
139
|
constructor(filename: string) {
|
|
104
|
-
this.db = new Database(filename);
|
|
105
|
-
this.db.pragma("journal_mode = WAL");
|
|
106
|
-
this.db.pragma("busy_timeout = 5000");
|
|
107
140
|
// FULL, not NORMAL: the outbox path makes durability non-negotiable
|
|
108
141
|
// (ADR constraint 5) — a queued send lost is a mail the user believes
|
|
109
142
|
// left but never did. FULL flushes the WAL on every commit, so a
|
|
110
143
|
// committed send survives host power-loss, not only a process restart.
|
|
111
144
|
// At this queue's write volume the extra fsync cost is irrelevant.
|
|
112
|
-
this.
|
|
145
|
+
this.handle = openSqlite({
|
|
146
|
+
filename,
|
|
147
|
+
mode: "create",
|
|
148
|
+
synchronous: "FULL",
|
|
149
|
+
schema,
|
|
150
|
+
});
|
|
151
|
+
this.db = this.handle.db;
|
|
113
152
|
this.migrate();
|
|
114
153
|
this.sequenceCounter = this.readMaxSequence();
|
|
115
154
|
}
|
|
116
155
|
|
|
117
156
|
private migrate(): void {
|
|
118
|
-
this.
|
|
157
|
+
this.handle.exec(`
|
|
119
158
|
CREATE TABLE IF NOT EXISTS queues (
|
|
120
159
|
name TEXT PRIMARY KEY,
|
|
121
160
|
fifo INTEGER NOT NULL DEFAULT 0,
|
|
@@ -159,11 +198,14 @@ export class QueueStore {
|
|
|
159
198
|
|
|
160
199
|
private readMaxSequence(): bigint {
|
|
161
200
|
const row = this.db
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
201
|
+
.select({
|
|
202
|
+
max: sql<
|
|
203
|
+
number | null
|
|
204
|
+
>`MAX(CAST(${messages.sequenceNumber} AS INTEGER))`,
|
|
205
|
+
})
|
|
206
|
+
.from(messages)
|
|
207
|
+
.get();
|
|
208
|
+
return BigInt(row?.max ?? 0);
|
|
167
209
|
}
|
|
168
210
|
|
|
169
211
|
private nextSequence(): string {
|
|
@@ -172,32 +214,22 @@ export class QueueStore {
|
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
close(): void {
|
|
175
|
-
this.
|
|
217
|
+
this.handle.close();
|
|
176
218
|
}
|
|
177
219
|
|
|
178
220
|
upsertQueue(def: QueueDefinition): QueueRecord {
|
|
221
|
+
const settings = {
|
|
222
|
+
fifo: def.fifo ? 1 : 0,
|
|
223
|
+
visibilityTimeoutSeconds: def.visibilityTimeoutSeconds,
|
|
224
|
+
deadLetterTargetName: def.deadLetterTargetName ?? null,
|
|
225
|
+
maxReceiveCount: def.maxReceiveCount ?? null,
|
|
226
|
+
contentBasedDeduplication: def.contentBasedDeduplication ? 1 : 0,
|
|
227
|
+
};
|
|
179
228
|
this.db
|
|
180
|
-
.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
content_based_deduplication
|
|
185
|
-
) VALUES (?, ?, ?, ?, ?, ?)
|
|
186
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
187
|
-
fifo = excluded.fifo,
|
|
188
|
-
visibility_timeout_seconds = excluded.visibility_timeout_seconds,
|
|
189
|
-
dead_letter_target_name = excluded.dead_letter_target_name,
|
|
190
|
-
max_receive_count = excluded.max_receive_count,
|
|
191
|
-
content_based_deduplication = excluded.content_based_deduplication`,
|
|
192
|
-
)
|
|
193
|
-
.run(
|
|
194
|
-
def.name,
|
|
195
|
-
def.fifo ? 1 : 0,
|
|
196
|
-
def.visibilityTimeoutSeconds,
|
|
197
|
-
def.deadLetterTargetName ?? null,
|
|
198
|
-
def.maxReceiveCount ?? null,
|
|
199
|
-
def.contentBasedDeduplication ? 1 : 0,
|
|
200
|
-
);
|
|
229
|
+
.insert(queues)
|
|
230
|
+
.values({ name: def.name, ...settings })
|
|
231
|
+
.onConflictDoUpdate({ target: queues.name, set: settings })
|
|
232
|
+
.run();
|
|
201
233
|
const created = this.getQueue(def.name);
|
|
202
234
|
if (!created) throw new Error(`queue ${def.name} vanished after upsert`);
|
|
203
235
|
return created;
|
|
@@ -205,15 +237,15 @@ export class QueueStore {
|
|
|
205
237
|
|
|
206
238
|
getQueue(name: string): QueueRecord | undefined {
|
|
207
239
|
const row = this.db
|
|
208
|
-
.
|
|
209
|
-
.
|
|
240
|
+
.select()
|
|
241
|
+
.from(queues)
|
|
242
|
+
.where(eq(queues.name, name))
|
|
243
|
+
.get();
|
|
210
244
|
return row ? toQueueRecord(row) : undefined;
|
|
211
245
|
}
|
|
212
246
|
|
|
213
247
|
listQueues(): QueueRecord[] {
|
|
214
|
-
const rows = this.db
|
|
215
|
-
.prepare("SELECT * FROM queues ORDER BY name")
|
|
216
|
-
.all() as QueueRow[];
|
|
248
|
+
const rows = this.db.select().from(queues).orderBy(asc(queues.name)).all();
|
|
217
249
|
return rows.map(toQueueRecord);
|
|
218
250
|
}
|
|
219
251
|
|
|
@@ -236,26 +268,29 @@ export class QueueStore {
|
|
|
236
268
|
(queue.contentBasedDeduplication ? md5 : undefined))
|
|
237
269
|
: undefined;
|
|
238
270
|
|
|
239
|
-
|
|
271
|
+
return this.db.transaction((): SendResult => {
|
|
240
272
|
if (dedupId) {
|
|
241
273
|
this.pruneDedup(input.queueName, now);
|
|
242
274
|
const existing = this.db
|
|
243
|
-
.
|
|
244
|
-
|
|
275
|
+
.select({ messageId: dedup.messageId })
|
|
276
|
+
.from(dedup)
|
|
277
|
+
.where(
|
|
278
|
+
and(
|
|
279
|
+
eq(dedup.queueName, input.queueName),
|
|
280
|
+
eq(dedup.dedupId, dedupId),
|
|
281
|
+
),
|
|
245
282
|
)
|
|
246
|
-
.get(
|
|
283
|
+
.get();
|
|
247
284
|
if (existing) {
|
|
248
285
|
const seq = this.db
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
.get(
|
|
253
|
-
| { sequence_number: string | null }
|
|
254
|
-
| undefined;
|
|
286
|
+
.select({ sequenceNumber: messages.sequenceNumber })
|
|
287
|
+
.from(messages)
|
|
288
|
+
.where(eq(messages.messageId, existing.messageId))
|
|
289
|
+
.get();
|
|
255
290
|
return {
|
|
256
|
-
messageId: existing.
|
|
291
|
+
messageId: existing.messageId,
|
|
257
292
|
md5OfBody: md5,
|
|
258
|
-
sequenceNumber: seq?.
|
|
293
|
+
sequenceNumber: seq?.sequenceNumber ?? null,
|
|
259
294
|
deduplicated: true,
|
|
260
295
|
};
|
|
261
296
|
}
|
|
@@ -263,31 +298,27 @@ export class QueueStore {
|
|
|
263
298
|
|
|
264
299
|
const messageId = randomUUID();
|
|
265
300
|
const sequenceNumber = queue.fifo ? this.nextSequence() : null;
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
input.queueName,
|
|
277
|
-
input.body,
|
|
278
|
-
md5,
|
|
279
|
-
input.groupId ?? null,
|
|
280
|
-
dedupId ?? null,
|
|
281
|
-
sequenceNumber,
|
|
282
|
-
now,
|
|
283
|
-
);
|
|
301
|
+
this.insertMessage({
|
|
302
|
+
messageId,
|
|
303
|
+
queueName: input.queueName,
|
|
304
|
+
body: input.body,
|
|
305
|
+
md5Body: md5,
|
|
306
|
+
groupId: input.groupId ?? null,
|
|
307
|
+
dedupId: dedupId ?? null,
|
|
308
|
+
sequenceNumber,
|
|
309
|
+
sentAt: now,
|
|
310
|
+
});
|
|
284
311
|
|
|
285
312
|
if (dedupId) {
|
|
286
313
|
this.db
|
|
287
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
314
|
+
.insert(dedup)
|
|
315
|
+
.values({
|
|
316
|
+
queueName: input.queueName,
|
|
317
|
+
dedupId,
|
|
318
|
+
messageId,
|
|
319
|
+
createdAt: now,
|
|
320
|
+
})
|
|
321
|
+
.run();
|
|
291
322
|
}
|
|
292
323
|
|
|
293
324
|
return {
|
|
@@ -296,9 +327,11 @@ export class QueueStore {
|
|
|
296
327
|
sequenceNumber,
|
|
297
328
|
deduplicated: false,
|
|
298
329
|
};
|
|
299
|
-
});
|
|
330
|
+
}, IMMEDIATE);
|
|
331
|
+
}
|
|
300
332
|
|
|
301
|
-
|
|
333
|
+
private insertMessage(message: typeof messages.$inferInsert): void {
|
|
334
|
+
this.db.insert(messages).values(message).run();
|
|
302
335
|
}
|
|
303
336
|
|
|
304
337
|
receiveMessages(input: {
|
|
@@ -312,7 +345,7 @@ export class QueueStore {
|
|
|
312
345
|
const visibilityTimeout =
|
|
313
346
|
input.visibilityTimeoutSeconds ?? queue.visibilityTimeoutSeconds;
|
|
314
347
|
|
|
315
|
-
const
|
|
348
|
+
const received = this.db.transaction((): ReceivedMessage[] => {
|
|
316
349
|
const picked: ReceivedMessage[] = [];
|
|
317
350
|
|
|
318
351
|
for (const candidate of this.nextCandidates(
|
|
@@ -323,49 +356,46 @@ export class QueueStore {
|
|
|
323
356
|
if (
|
|
324
357
|
queue.maxReceiveCount !== null &&
|
|
325
358
|
queue.deadLetterTargetName !== null &&
|
|
326
|
-
candidate.
|
|
359
|
+
candidate.receiveCount >= queue.maxReceiveCount
|
|
327
360
|
) {
|
|
328
361
|
this.db
|
|
329
|
-
.
|
|
330
|
-
.
|
|
362
|
+
.update(messages)
|
|
363
|
+
.set({ queueName: queue.deadLetterTargetName })
|
|
364
|
+
.where(eq(messages.id, candidate.id))
|
|
365
|
+
.run();
|
|
331
366
|
continue;
|
|
332
367
|
}
|
|
333
368
|
|
|
334
369
|
const receiptHandle = randomUUID();
|
|
335
|
-
const receiveCount = candidate.
|
|
336
|
-
const firstReceived = candidate.
|
|
370
|
+
const receiveCount = candidate.receiveCount + 1;
|
|
371
|
+
const firstReceived = candidate.firstReceivedAt ?? now;
|
|
337
372
|
this.db
|
|
338
|
-
.
|
|
339
|
-
|
|
340
|
-
receive_count = ?, visible_at = ?, receipt_handle = ?,
|
|
341
|
-
first_received_at = ?
|
|
342
|
-
WHERE id = ?`,
|
|
343
|
-
)
|
|
344
|
-
.run(
|
|
373
|
+
.update(messages)
|
|
374
|
+
.set({
|
|
345
375
|
receiveCount,
|
|
346
|
-
now + visibilityTimeout * 1000,
|
|
376
|
+
visibleAt: now + visibilityTimeout * 1000,
|
|
347
377
|
receiptHandle,
|
|
348
|
-
firstReceived,
|
|
349
|
-
|
|
350
|
-
)
|
|
378
|
+
firstReceivedAt: firstReceived,
|
|
379
|
+
})
|
|
380
|
+
.where(eq(messages.id, candidate.id))
|
|
381
|
+
.run();
|
|
351
382
|
|
|
352
383
|
picked.push({
|
|
353
|
-
messageId: candidate.
|
|
384
|
+
messageId: candidate.messageId,
|
|
354
385
|
body: candidate.body,
|
|
355
|
-
md5OfBody: candidate.
|
|
386
|
+
md5OfBody: candidate.md5Body,
|
|
356
387
|
receiptHandle,
|
|
357
388
|
receiveCount,
|
|
358
|
-
sentTimestamp: candidate.
|
|
389
|
+
sentTimestamp: candidate.sentAt,
|
|
359
390
|
firstReceivedTimestamp: firstReceived,
|
|
360
|
-
groupId: candidate.
|
|
361
|
-
sequenceNumber: candidate.
|
|
391
|
+
groupId: candidate.groupId,
|
|
392
|
+
sequenceNumber: candidate.sequenceNumber,
|
|
362
393
|
});
|
|
363
394
|
}
|
|
364
395
|
|
|
365
396
|
return picked;
|
|
366
|
-
});
|
|
397
|
+
}, IMMEDIATE);
|
|
367
398
|
|
|
368
|
-
const received = receive();
|
|
369
399
|
if (received.length > 0) this.lastReceiveAt.set(input.queueName, now);
|
|
370
400
|
return received;
|
|
371
401
|
}
|
|
@@ -379,33 +409,41 @@ export class QueueStore {
|
|
|
379
409
|
deleteMessage(queueName: string, receiptHandle: string): void {
|
|
380
410
|
this.requireQueue(queueName);
|
|
381
411
|
this.db
|
|
382
|
-
.
|
|
383
|
-
|
|
412
|
+
.delete(messages)
|
|
413
|
+
.where(
|
|
414
|
+
and(
|
|
415
|
+
eq(messages.queueName, queueName),
|
|
416
|
+
eq(messages.receiptHandle, receiptHandle),
|
|
417
|
+
),
|
|
384
418
|
)
|
|
385
|
-
.run(
|
|
419
|
+
.run();
|
|
386
420
|
}
|
|
387
421
|
|
|
388
422
|
purgeQueue(queueName: string): void {
|
|
389
423
|
this.requireQueue(queueName);
|
|
390
|
-
this.db.
|
|
424
|
+
this.db.delete(messages).where(eq(messages.queueName, queueName)).run();
|
|
391
425
|
}
|
|
392
426
|
|
|
393
427
|
getQueueAttributes(queueName: string, now = Date.now()): QueueAttributes {
|
|
394
428
|
const queue = this.requireQueue(queueName);
|
|
395
429
|
const visible = this.db
|
|
396
|
-
.
|
|
397
|
-
|
|
430
|
+
.select({ n: count() })
|
|
431
|
+
.from(messages)
|
|
432
|
+
.where(
|
|
433
|
+
and(eq(messages.queueName, queueName), lte(messages.visibleAt, now)),
|
|
398
434
|
)
|
|
399
|
-
.get(
|
|
435
|
+
.get();
|
|
400
436
|
const notVisible = this.db
|
|
401
|
-
.
|
|
402
|
-
|
|
437
|
+
.select({ n: count() })
|
|
438
|
+
.from(messages)
|
|
439
|
+
.where(
|
|
440
|
+
and(eq(messages.queueName, queueName), gt(messages.visibleAt, now)),
|
|
403
441
|
)
|
|
404
|
-
.get(
|
|
442
|
+
.get();
|
|
405
443
|
|
|
406
444
|
return {
|
|
407
|
-
approximateNumberOfMessages: visible
|
|
408
|
-
approximateNumberOfMessagesNotVisible: notVisible
|
|
445
|
+
approximateNumberOfMessages: visible?.n ?? 0,
|
|
446
|
+
approximateNumberOfMessagesNotVisible: notVisible?.n ?? 0,
|
|
409
447
|
visibilityTimeout: queue.visibilityTimeoutSeconds,
|
|
410
448
|
fifoQueue: queue.fifo,
|
|
411
449
|
redrivePolicy:
|
|
@@ -425,34 +463,42 @@ export class QueueStore {
|
|
|
425
463
|
): MessageRow[] {
|
|
426
464
|
if (!queue.fifo) {
|
|
427
465
|
return this.db
|
|
428
|
-
.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
466
|
+
.select()
|
|
467
|
+
.from(messages)
|
|
468
|
+
.where(
|
|
469
|
+
and(eq(messages.queueName, queue.name), lte(messages.visibleAt, now)),
|
|
432
470
|
)
|
|
433
|
-
.
|
|
471
|
+
.orderBy(asc(messages.id))
|
|
472
|
+
.limit(limit)
|
|
473
|
+
.all();
|
|
434
474
|
}
|
|
435
475
|
|
|
476
|
+
const groupHeads = sql`WITH RECURSIVE g(gid) AS (
|
|
477
|
+
SELECT MIN(group_id) FROM messages WHERE queue_name = ${queue.name}
|
|
478
|
+
UNION ALL
|
|
479
|
+
SELECT (
|
|
480
|
+
SELECT MIN(group_id) FROM messages
|
|
481
|
+
WHERE queue_name = ${queue.name} AND group_id > g.gid
|
|
482
|
+
)
|
|
483
|
+
FROM g WHERE g.gid IS NOT NULL
|
|
484
|
+
)
|
|
485
|
+
SELECT (
|
|
486
|
+
SELECT MIN(id) FROM messages
|
|
487
|
+
WHERE queue_name = ${queue.name} AND group_id = g.gid
|
|
488
|
+
) FROM g`;
|
|
489
|
+
|
|
436
490
|
return this.db
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
)
|
|
445
|
-
FROM g WHERE g.gid IS NOT NULL
|
|
446
|
-
)
|
|
447
|
-
SELECT m.* FROM g
|
|
448
|
-
JOIN messages m ON m.id = (
|
|
449
|
-
SELECT MIN(id) FROM messages
|
|
450
|
-
WHERE queue_name = @queue AND group_id = g.gid
|
|
451
|
-
)
|
|
452
|
-
WHERE m.visible_at <= @now
|
|
453
|
-
ORDER BY m.id ASC LIMIT @limit`,
|
|
491
|
+
.select()
|
|
492
|
+
.from(messages)
|
|
493
|
+
.where(
|
|
494
|
+
and(
|
|
495
|
+
sql`${messages.id} IN (${groupHeads})`,
|
|
496
|
+
lte(messages.visibleAt, now),
|
|
497
|
+
),
|
|
454
498
|
)
|
|
455
|
-
.
|
|
499
|
+
.orderBy(asc(messages.id))
|
|
500
|
+
.limit(limit)
|
|
501
|
+
.all();
|
|
456
502
|
}
|
|
457
503
|
|
|
458
504
|
/**
|
|
@@ -473,29 +519,33 @@ export class QueueStore {
|
|
|
473
519
|
}
|
|
474
520
|
const destination = source.name;
|
|
475
521
|
|
|
476
|
-
|
|
522
|
+
return this.db.transaction((): number => {
|
|
477
523
|
const rows = this.db
|
|
478
|
-
.
|
|
479
|
-
.
|
|
524
|
+
.select()
|
|
525
|
+
.from(messages)
|
|
526
|
+
.where(eq(messages.queueName, deadLetterName))
|
|
527
|
+
.orderBy(asc(messages.id))
|
|
528
|
+
.all();
|
|
480
529
|
for (const row of rows) this.moveToQueue(row, destination, now);
|
|
481
530
|
return rows.length;
|
|
482
|
-
});
|
|
483
|
-
return redrive();
|
|
531
|
+
}, IMMEDIATE);
|
|
484
532
|
}
|
|
485
533
|
|
|
486
534
|
redriveForRelease(release: string, deadLetterNames: string[]): void {
|
|
487
535
|
this.db.transaction((): void => {
|
|
488
536
|
const marker = this.db
|
|
489
|
-
.
|
|
490
|
-
.
|
|
537
|
+
.select({ release: releaseRedrive.release })
|
|
538
|
+
.from(releaseRedrive)
|
|
539
|
+
.where(eq(releaseRedrive.id, 1))
|
|
540
|
+
.get();
|
|
491
541
|
if (marker?.release === release) return;
|
|
492
542
|
for (const name of deadLetterNames) this.redriveDeadLetters(name);
|
|
493
543
|
this.db
|
|
494
|
-
.
|
|
495
|
-
|
|
496
|
-
)
|
|
497
|
-
.run(
|
|
498
|
-
})
|
|
544
|
+
.insert(releaseRedrive)
|
|
545
|
+
.values({ id: 1, release })
|
|
546
|
+
.onConflictDoUpdate({ target: releaseRedrive.id, set: { release } })
|
|
547
|
+
.run();
|
|
548
|
+
}, IMMEDIATE);
|
|
499
549
|
}
|
|
500
550
|
|
|
501
551
|
private moveToQueue(
|
|
@@ -503,31 +553,29 @@ export class QueueStore {
|
|
|
503
553
|
targetName: string,
|
|
504
554
|
now: number,
|
|
505
555
|
): void {
|
|
506
|
-
this.db.
|
|
507
|
-
this.
|
|
508
|
-
.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
targetName,
|
|
518
|
-
message.body,
|
|
519
|
-
message.md5_body,
|
|
520
|
-
message.group_id,
|
|
521
|
-
message.dedup_id,
|
|
522
|
-
message.sequence_number,
|
|
523
|
-
now,
|
|
524
|
-
);
|
|
556
|
+
this.db.delete(messages).where(eq(messages.id, message.id)).run();
|
|
557
|
+
this.insertMessage({
|
|
558
|
+
messageId: message.messageId,
|
|
559
|
+
queueName: targetName,
|
|
560
|
+
body: message.body,
|
|
561
|
+
md5Body: message.md5Body,
|
|
562
|
+
groupId: message.groupId,
|
|
563
|
+
dedupId: message.dedupId,
|
|
564
|
+
sequenceNumber: message.sequenceNumber,
|
|
565
|
+
sentAt: now,
|
|
566
|
+
});
|
|
525
567
|
}
|
|
526
568
|
|
|
527
569
|
private pruneDedup(queueName: string, now: number): void {
|
|
528
570
|
this.db
|
|
529
|
-
.
|
|
530
|
-
.
|
|
571
|
+
.delete(dedup)
|
|
572
|
+
.where(
|
|
573
|
+
and(
|
|
574
|
+
eq(dedup.queueName, queueName),
|
|
575
|
+
lt(dedup.createdAt, now - DEDUP_WINDOW_MS),
|
|
576
|
+
),
|
|
577
|
+
)
|
|
578
|
+
.run();
|
|
531
579
|
}
|
|
532
580
|
|
|
533
581
|
private requireQueue(name: string): QueueRecord {
|