@maestro-js/queue 1.0.0-alpha.19 → 1.0.0-alpha.2
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/README.md +179 -337
- package/dist/index.d.ts +215 -179
- package/dist/index.js +475 -422
- package/package.json +6 -10
package/dist/index.js
CHANGED
|
@@ -1,491 +1,544 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { instantFns as instantFns2 } from "iso-fns2";
|
|
3
|
+
import { Log } from "@maestro-js/log";
|
|
4
4
|
import { ServiceRegistry } from "@maestro-js/service-registry";
|
|
5
5
|
|
|
6
6
|
// src/db-queue-driver.ts
|
|
7
|
-
import
|
|
7
|
+
import crypto from "crypto";
|
|
8
8
|
import { instantFns } from "iso-fns2";
|
|
9
|
+
import assert from "assert";
|
|
9
10
|
function DbQueueDriver({
|
|
10
11
|
db,
|
|
11
12
|
databaseTable,
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
extraFields,
|
|
14
|
+
fastestPollingRateMs = 100,
|
|
15
|
+
slowestPollingRateMs = 1e4,
|
|
16
|
+
pollingBackoffRate = 1.1
|
|
14
17
|
}) {
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
function logError(msg) {
|
|
19
|
+
console.error(`Oxen Queue: ${msg}`);
|
|
17
20
|
}
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const rows = await db.select(
|
|
22
|
-
`SELECT COUNT(*) AS count FROM ${databaseTable} WHERE status = 'waiting' AND scheduledDate <= NOW() ${queueFilterClause}`,
|
|
23
|
-
queueFilterParams
|
|
24
|
-
);
|
|
25
|
-
return Number(rows[0]?.["count"] ?? 0);
|
|
21
|
+
const processingMap = /* @__PURE__ */ new Map();
|
|
22
|
+
function getIsProcessing(queueName) {
|
|
23
|
+
return !!processingMap.get(queueName);
|
|
26
24
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`INSERT INTO ${databaseTable} (queue, body, scheduledDate) VALUES (?, ?, GREATEST(?, NOW()))`,
|
|
30
|
-
[message.queue, JSON.stringify(message.body), instantFns.formatISO9075(message.scheduledDate)]
|
|
31
|
-
);
|
|
32
|
-
return String(insertId);
|
|
25
|
+
function setIsProcessing(queueName, processing) {
|
|
26
|
+
processingMap.set(queueName, processing);
|
|
33
27
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const params = [];
|
|
38
|
-
for (const message of messages) {
|
|
39
|
-
params.push(message.queue, JSON.stringify(message.body), instantFns.formatISO9075(message.scheduledDate));
|
|
40
|
-
}
|
|
41
|
-
await db.insert(`INSERT INTO ${databaseTable} (queue, body, scheduledDate) VALUES ${placeholders}`, params);
|
|
28
|
+
const jobRecoveryIntervalMap = /* @__PURE__ */ new Map();
|
|
29
|
+
function getJobRecoveryInterval(queueName) {
|
|
30
|
+
return jobRecoveryIntervalMap.get(queueName) ?? null;
|
|
42
31
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
function setJobRecoveryInterval(queueName, jobRecoveryInterval) {
|
|
33
|
+
jobRecoveryIntervalMap.set(queueName, jobRecoveryInterval);
|
|
34
|
+
}
|
|
35
|
+
async function start({
|
|
36
|
+
workFn,
|
|
37
|
+
concurrency = 3,
|
|
38
|
+
timeout = 60 * 45,
|
|
39
|
+
shouldRecoverStuckJobs = true,
|
|
40
|
+
queueName
|
|
41
|
+
}) {
|
|
42
|
+
if (getIsProcessing(queueName)) {
|
|
43
|
+
throw new Error(`This queue is already processing`);
|
|
50
44
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
SELECT id, attempt FROM ${databaseTable} FORCE INDEX (locking_pop)
|
|
56
|
-
WHERE lockId IS NULL
|
|
57
|
-
AND status = 'waiting'
|
|
58
|
-
AND scheduledDate <= NOW()
|
|
59
|
-
${queueFilterClause}
|
|
60
|
-
ORDER BY scheduledDate ASC
|
|
61
|
-
LIMIT ?
|
|
62
|
-
) AS sub ON sub.id = ${databaseTable}.id AND sub.attempt = ${databaseTable}.attempt
|
|
63
|
-
SET lockId = ?, status = 'processing', startedDate = NOW()`,
|
|
64
|
-
[...queueFilterParams, count, lockId]
|
|
65
|
-
);
|
|
66
|
-
if (locked.changedRows === 0) {
|
|
67
|
-
return [];
|
|
45
|
+
if (!workFn) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Missing work_fn argument, nothing to do! Remember that the process() function takes an object as its single argument.`
|
|
48
|
+
);
|
|
68
49
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}) {
|
|
113
|
-
let waiting = [];
|
|
114
|
-
function processBatch() {
|
|
115
|
-
const batch = waiting;
|
|
116
|
-
waiting = [];
|
|
117
|
-
batchPop(batch.length).then((messages) => {
|
|
118
|
-
batch.forEach((b, index) => {
|
|
119
|
-
if (index < messages.length) {
|
|
120
|
-
b[0](messages[index]);
|
|
121
|
-
} else {
|
|
122
|
-
b[0](null);
|
|
123
|
-
}
|
|
50
|
+
if (typeof concurrency !== "number") {
|
|
51
|
+
throw new Error(`The concurrency argument must be a number`);
|
|
52
|
+
}
|
|
53
|
+
let jobTimeoutSeconds = Math.floor(timeout);
|
|
54
|
+
let inProcess = 0;
|
|
55
|
+
let pollingRate = fastestPollingRateMs;
|
|
56
|
+
let currentlyFetching = false;
|
|
57
|
+
const workingJobBatch = [];
|
|
58
|
+
setIsProcessing(queueName, true);
|
|
59
|
+
async function fillJobBatch() {
|
|
60
|
+
const batchId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
|
61
|
+
const lockedBatch = await db.update(
|
|
62
|
+
`
|
|
63
|
+
UPDATE ${databaseTable} AS main
|
|
64
|
+
INNER JOIN (
|
|
65
|
+
SELECT id FROM ${databaseTable} FORCE INDEX (locking_update)
|
|
66
|
+
WHERE batchId IS NULL
|
|
67
|
+
AND STATUS = "waiting"
|
|
68
|
+
AND jobType = ?
|
|
69
|
+
AND scheduledDate <= NOW()
|
|
70
|
+
ORDER BY priority ASC LIMIT ${concurrency}
|
|
71
|
+
) sub
|
|
72
|
+
ON sub.id = main.id
|
|
73
|
+
SET batchId = ?, STATUS = "processing", startedDate = NOW()`,
|
|
74
|
+
[queueName, batchId]
|
|
75
|
+
);
|
|
76
|
+
if (lockedBatch.changedRows === 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const nextJobs = await db.select(
|
|
80
|
+
`SELECT id, scheduledDate, body, trace FROM ${databaseTable} WHERE batchId = ? ORDER BY priority ASC LIMIT ${concurrency}`,
|
|
81
|
+
[batchId]
|
|
82
|
+
);
|
|
83
|
+
if (nextJobs.length === 0) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
nextJobs.map((db2) => ({
|
|
87
|
+
id: db2.id,
|
|
88
|
+
scheduledDate: new Date(db2.scheduledDate).toISOString(),
|
|
89
|
+
body: db2.body ? JSON.parse(db2.body) : db2.body,
|
|
90
|
+
trace: db2.trace ? JSON.parse(db2.trace) : db2.trace
|
|
91
|
+
})).forEach((job) => {
|
|
92
|
+
workingJobBatch.push(job);
|
|
124
93
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
async function getNextJob() {
|
|
97
|
+
if (!currentlyFetching && workingJobBatch.length < concurrency) {
|
|
98
|
+
currentlyFetching = true;
|
|
99
|
+
await fillJobBatch().catch((error) => {
|
|
100
|
+
logError("There was an error while trying to get the next set of jobs:");
|
|
101
|
+
logError(error);
|
|
102
|
+
});
|
|
103
|
+
currentlyFetching = false;
|
|
134
104
|
}
|
|
135
|
-
return
|
|
105
|
+
return workingJobBatch.shift();
|
|
136
106
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
107
|
+
async function doWork() {
|
|
108
|
+
const job = await getNextJob();
|
|
109
|
+
if (!job) {
|
|
110
|
+
if (pollingRate < slowestPollingRateMs) {
|
|
111
|
+
pollingRate *= pollingBackoffRate;
|
|
112
|
+
}
|
|
113
|
+
if (pollingRate >= slowestPollingRateMs) {
|
|
114
|
+
pollingRate = slowestPollingRateMs;
|
|
115
|
+
}
|
|
116
|
+
return Promise.resolve("no_job");
|
|
117
|
+
}
|
|
118
|
+
pollingRate = fastestPollingRateMs;
|
|
119
|
+
const promises = [
|
|
120
|
+
workFn({ body: job.body, id: job.id, trace: job.trace, scheduledDate: job.scheduledDate }),
|
|
121
|
+
new Promise((_, reject) => {
|
|
122
|
+
setTimeout(
|
|
123
|
+
() => reject(new Error(`timeout for job_id ${job.id} (over ${jobTimeoutSeconds} seconds)`)),
|
|
124
|
+
typeof jobTimeoutSeconds === "number" ? jobTimeoutSeconds * 1e3 : 0
|
|
125
|
+
);
|
|
126
|
+
})
|
|
127
|
+
];
|
|
128
|
+
return Promise.race(promises).then(async (jobResult) => {
|
|
129
|
+
return db.update(
|
|
130
|
+
`
|
|
131
|
+
update ${databaseTable}
|
|
132
|
+
set
|
|
133
|
+
result = ?,
|
|
134
|
+
uniqueKey = NULL,
|
|
135
|
+
status="success",
|
|
136
|
+
runningTimeSeconds = TIMESTAMPDIFF(SECOND,startedDate,NOW())
|
|
137
|
+
where id =?
|
|
138
|
+
LIMIT 1`,
|
|
139
|
+
[JSON.stringify(jobResult), job.id]
|
|
140
|
+
);
|
|
141
|
+
}).catch(async (error) => {
|
|
142
|
+
return db.update(
|
|
143
|
+
`
|
|
144
|
+
update ${databaseTable}
|
|
145
|
+
set
|
|
146
|
+
result = ?,
|
|
147
|
+
uniqueKey = NULL,
|
|
148
|
+
status="error",
|
|
149
|
+
runningTimeSeconds = TIMESTAMPDIFF(SECOND,startedDate,NOW())
|
|
150
|
+
where id = ?
|
|
151
|
+
LIMIT 1`,
|
|
152
|
+
[error.stack, job.id]
|
|
153
|
+
);
|
|
184
154
|
});
|
|
185
155
|
}
|
|
156
|
+
async function loop() {
|
|
157
|
+
if (inProcess < concurrency) {
|
|
158
|
+
inProcess++;
|
|
159
|
+
doWork().then(() => {
|
|
160
|
+
inProcess--;
|
|
161
|
+
}).catch((error) => {
|
|
162
|
+
inProcess--;
|
|
163
|
+
logError("Unhandled Oxen Queue error:");
|
|
164
|
+
logError(error);
|
|
165
|
+
});
|
|
166
|
+
} else if (pollingRate < slowestPollingRateMs) {
|
|
167
|
+
pollingRate *= pollingBackoffRate;
|
|
168
|
+
if (pollingRate >= slowestPollingRateMs) {
|
|
169
|
+
pollingRate = slowestPollingRateMs;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!getIsProcessing(queueName)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
setTimeout(function() {
|
|
176
|
+
if (!getIsProcessing(queueName)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
loop();
|
|
180
|
+
}, pollingRate ?? 0);
|
|
181
|
+
}
|
|
182
|
+
async function recoverStuckJobs() {
|
|
183
|
+
return db.update(
|
|
184
|
+
`
|
|
185
|
+
UPDATE ${databaseTable}
|
|
186
|
+
SET
|
|
187
|
+
STATUS="waiting",
|
|
188
|
+
batchId = NULL,
|
|
189
|
+
startedDate = NULL,
|
|
190
|
+
recovered = 1
|
|
191
|
+
WHERE
|
|
192
|
+
STATUS="processing" AND
|
|
193
|
+
startedDate < (NOW() - INTERVAL ${jobTimeoutSeconds} SECOND) AND
|
|
194
|
+
jobType = ?
|
|
195
|
+
`,
|
|
196
|
+
[queueName]
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
async function markStuckJobs() {
|
|
200
|
+
return db.update(
|
|
201
|
+
`
|
|
202
|
+
UPDATE ${databaseTable}
|
|
203
|
+
SET
|
|
204
|
+
STATUS="stuck",
|
|
205
|
+
uniqueKey = NULL,
|
|
206
|
+
recovered = 1
|
|
207
|
+
WHERE
|
|
208
|
+
STATUS="processing" AND
|
|
209
|
+
startedDate < (NOW() - INTERVAL ${jobTimeoutSeconds} SECOND) AND
|
|
210
|
+
jobType = ?
|
|
211
|
+
`,
|
|
212
|
+
[queueName]
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
loop();
|
|
216
|
+
setJobRecoveryInterval(
|
|
217
|
+
queueName,
|
|
218
|
+
setInterval(function() {
|
|
219
|
+
if (shouldRecoverStuckJobs) {
|
|
220
|
+
recoverStuckJobs().catch(function(error) {
|
|
221
|
+
logError("Unable to recover stuck jobs:");
|
|
222
|
+
logError(error);
|
|
223
|
+
});
|
|
224
|
+
} else {
|
|
225
|
+
markStuckJobs().catch(function(error) {
|
|
226
|
+
logError("Unable to mark stuck jobs:");
|
|
227
|
+
logError(error);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}, 1e3 * 60)
|
|
231
|
+
//every minute
|
|
232
|
+
);
|
|
186
233
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
msg.status = "processing";
|
|
192
|
-
return { id: String(msg.id), queue: msg.queue, body: structuredClone(msg.body), attempts: msg.attempt };
|
|
234
|
+
function stop(options) {
|
|
235
|
+
setIsProcessing(options.queueName, false);
|
|
236
|
+
const jobRecoveryInterval = getJobRecoveryInterval(options.queueName);
|
|
237
|
+
if (jobRecoveryInterval) clearInterval(jobRecoveryInterval);
|
|
193
238
|
}
|
|
194
|
-
async function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
239
|
+
async function listMessages({
|
|
240
|
+
pageNumber,
|
|
241
|
+
pageSize,
|
|
242
|
+
queueName
|
|
243
|
+
}) {
|
|
244
|
+
const skip = pageNumber * pageSize;
|
|
245
|
+
const results = await db.select(`SELECT * FROM ${databaseTable} WHERE jobType = ? ORDER BY id DESC LIMIT ? OFFSET ?`, [
|
|
246
|
+
queueName,
|
|
247
|
+
pageSize,
|
|
248
|
+
skip
|
|
249
|
+
]);
|
|
250
|
+
return results.map((r) => dbToJob(r));
|
|
200
251
|
}
|
|
201
|
-
async function
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
252
|
+
async function addMessage(job) {
|
|
253
|
+
const uniqueKey = job.uniqueKey === void 0 ? null : parseInt(crypto.createHash("md5").update(`${job.uniqueKey}|${job.queueName}`).digest("hex").slice(0, 8), 16);
|
|
254
|
+
const inserts = [
|
|
255
|
+
JSON.stringify(job.body),
|
|
256
|
+
job.traceData ? JSON.stringify(job.traceData) : null,
|
|
257
|
+
job.queueName,
|
|
258
|
+
uniqueKey,
|
|
259
|
+
typeof job.priority === "number" ? job.priority : Date.now()
|
|
260
|
+
];
|
|
261
|
+
extraFields?.forEach((field) => {
|
|
262
|
+
inserts.push(job[field]);
|
|
263
|
+
});
|
|
264
|
+
const fields = ["body", "trace", "jobType", "uniqueKey", "priority", ...extraFields ?? []];
|
|
265
|
+
const { insertId } = await db.insert(
|
|
266
|
+
`
|
|
267
|
+
insert into ${databaseTable} (
|
|
268
|
+
${fields.join(",")}, scheduledDate
|
|
269
|
+
)
|
|
270
|
+
VALUES (?, GREATEST(?, NOW()))
|
|
271
|
+
ON DUPLICATE KEY UPDATE
|
|
272
|
+
priority = IF(priority > VALUES(priority), VALUES(priority), priority)
|
|
273
|
+
`,
|
|
274
|
+
[inserts, instantFns.formatISO9075(job.scheduledDate ?? instantFns.now())]
|
|
275
|
+
);
|
|
276
|
+
if (!insertId) {
|
|
277
|
+
const results = await db.select(`SELECT id FROM ${databaseTable} WHERE uniqueKey = ?`, [uniqueKey]);
|
|
278
|
+
const row = results[0];
|
|
279
|
+
assert.ok(row, "Could not find inserted row");
|
|
280
|
+
return { id: row["id"] };
|
|
281
|
+
} else {
|
|
282
|
+
return { id: insertId };
|
|
206
283
|
}
|
|
207
284
|
}
|
|
208
|
-
async function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
285
|
+
async function updateMessage({
|
|
286
|
+
queueName,
|
|
287
|
+
messageId,
|
|
288
|
+
scheduledDate,
|
|
289
|
+
body
|
|
213
290
|
}) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
291
|
+
const result = instantFns.formatISO9075(scheduledDate);
|
|
292
|
+
await db.update(
|
|
293
|
+
`UPDATE ${databaseTable} SET scheduledDate = GREATEST(?, NOW()), body = ? WHERE jobType = ? AND id = ? AND status = "waiting"`,
|
|
294
|
+
[result, JSON.stringify(body), queueName, messageId]
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
async function isQueueEmpty({ queueName }) {
|
|
298
|
+
const results = await db.select(
|
|
299
|
+
`
|
|
300
|
+
SELECT id FROM ${databaseTable} FORCE INDEX (locking_update)
|
|
301
|
+
WHERE ((batchId IS NULL AND STATUS = "waiting") OR STATUS = "processing")
|
|
302
|
+
AND jobType = ?
|
|
303
|
+
AND scheduledDate <= NOW()
|
|
304
|
+
ORDER BY priority ASC LIMIT 1`,
|
|
305
|
+
[queueName]
|
|
306
|
+
);
|
|
307
|
+
return !results.length;
|
|
227
308
|
}
|
|
228
|
-
return {
|
|
309
|
+
return { start, stop, listMessages, addMessage, updateMessage, isQueueEmpty };
|
|
229
310
|
}
|
|
230
|
-
|
|
231
|
-
// src/index.ts
|
|
232
|
-
function combineLoggers(...loggers) {
|
|
233
|
-
const active = loggers.filter((l) => !!l);
|
|
311
|
+
function dbToJob(db) {
|
|
234
312
|
return {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
313
|
+
id: db.id,
|
|
314
|
+
batchId: db.batchId,
|
|
315
|
+
queue: db.jobType,
|
|
316
|
+
scheduledDate: new Date(db.scheduledDate).toISOString(),
|
|
317
|
+
enqueuedDate: new Date(db.enqueuedDate).toISOString(),
|
|
318
|
+
startedDate: db.startedDate ? new Date(db.startedDate).toISOString() : null,
|
|
319
|
+
body: db.body ? JSON.parse(db.body) : db.body,
|
|
320
|
+
trace: db.trace ? JSON.parse(db.trace) : db.trace,
|
|
321
|
+
status: db.status,
|
|
322
|
+
result: db.result,
|
|
323
|
+
recovered: db.recovered,
|
|
324
|
+
runningTimeSeconds: db.runningTimeSeconds,
|
|
325
|
+
uniqueKey: db.uniqueKey,
|
|
326
|
+
priority: db.priority
|
|
244
327
|
};
|
|
245
328
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
329
|
+
|
|
330
|
+
// src/index.ts
|
|
331
|
+
var noopTracing = {
|
|
332
|
+
hydrate: (_trace, callback) => callback(),
|
|
333
|
+
dehydrate: () => null,
|
|
334
|
+
startActiveSpan: (_name, fn) => fn(),
|
|
335
|
+
getActiveSpan: () => void 0
|
|
336
|
+
};
|
|
337
|
+
function createService(config) {
|
|
338
|
+
const queues = [];
|
|
339
|
+
const tracing = config.tracing ?? noopTracing;
|
|
340
|
+
function list() {
|
|
341
|
+
return queues.map((q) => q.name);
|
|
259
342
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
fail(error) {
|
|
263
|
-
throw new FailQueueMessageError(error);
|
|
264
|
-
},
|
|
265
|
-
release(error, delaySeconds) {
|
|
266
|
-
throw new ReleaseQueueMessageError(error, delaySeconds);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
const middleware = options.queueConfig.middleware ?? [];
|
|
270
|
-
const executeFn = middleware.reduceRight(
|
|
271
|
-
(acc, cur) => {
|
|
272
|
-
return (body) => cur(body, { ...actions, next: () => acc(body) });
|
|
273
|
-
},
|
|
274
|
-
(body) => options.queueConfig.handle(body, actions)
|
|
275
|
-
);
|
|
276
|
-
return executeFn(options.queueMessage.body);
|
|
343
|
+
function get(name) {
|
|
344
|
+
return queues.find((q) => q.name === name);
|
|
277
345
|
}
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
await
|
|
346
|
+
function create(options) {
|
|
347
|
+
const queueName = options.name;
|
|
348
|
+
const concurrency = options.concurrency ?? 1;
|
|
349
|
+
const logger = Log.create({
|
|
350
|
+
transports: [config.logger, ...options.logger ? [options.logger] : []]
|
|
351
|
+
});
|
|
352
|
+
async function workFn({
|
|
353
|
+
body,
|
|
354
|
+
id,
|
|
355
|
+
trace,
|
|
356
|
+
scheduledDate
|
|
357
|
+
}) {
|
|
358
|
+
try {
|
|
359
|
+
logger.info({
|
|
360
|
+
event: "messageDequeued",
|
|
361
|
+
queue: queueName,
|
|
362
|
+
driver: config.driver,
|
|
363
|
+
body,
|
|
364
|
+
messageId: id
|
|
365
|
+
});
|
|
366
|
+
const result = await tracing.hydrate(trace, async () => {
|
|
367
|
+
return tracing.startActiveSpan(
|
|
368
|
+
{ name: "queue_consumer_transaction", attributes: { "messaging.destination.name": options.name } },
|
|
369
|
+
() => {
|
|
370
|
+
const parent = tracing.getActiveSpan();
|
|
371
|
+
return tracing.startActiveSpan(
|
|
372
|
+
{
|
|
373
|
+
name: "queue_consumer",
|
|
374
|
+
attributes: {
|
|
375
|
+
op: "queue.process",
|
|
376
|
+
"sentry.op": "queue.process",
|
|
377
|
+
"messaging.message.id": id,
|
|
378
|
+
"messaging.destination.name": options.name,
|
|
379
|
+
"messaging.message.receive.latency": instantFns2.getEpochMilliseconds(instantFns2.now()) - instantFns2.getEpochMilliseconds(scheduledDate)
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
async () => {
|
|
383
|
+
const res = await options.workFn(body);
|
|
384
|
+
parent?.setStatus({ code: 1, message: "ok" });
|
|
385
|
+
return res;
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
logger.info({
|
|
392
|
+
event: "messageProcessed",
|
|
393
|
+
queue: queueName,
|
|
394
|
+
driver: config.driver,
|
|
395
|
+
body,
|
|
396
|
+
result,
|
|
397
|
+
messageId: id
|
|
398
|
+
});
|
|
399
|
+
return result;
|
|
400
|
+
} catch (e) {
|
|
299
401
|
logger.error({
|
|
300
402
|
event: "messageFailed",
|
|
301
|
-
queue:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
error: e
|
|
403
|
+
queue: queueName,
|
|
404
|
+
driver: config.driver,
|
|
405
|
+
body,
|
|
406
|
+
error: e,
|
|
407
|
+
messageId: id
|
|
305
408
|
});
|
|
306
409
|
throw e;
|
|
307
|
-
} else {
|
|
308
|
-
const isLastAttempt = queueMessage.attempts + 1 >= (queueConfig.tries ?? 1);
|
|
309
|
-
const error = e instanceof ReleaseQueueMessageError ? e.cause : e instanceof Error ? e : new Error(String(e));
|
|
310
|
-
if (isLastAttempt) {
|
|
311
|
-
await driver.fail({ id: queueMessage.id, attempts: queueMessage.attempts, error });
|
|
312
|
-
logger.error({
|
|
313
|
-
event: "messageFailed",
|
|
314
|
-
queue: queueMessage.queue,
|
|
315
|
-
body: queueMessage.body,
|
|
316
|
-
messageId: queueMessage.id,
|
|
317
|
-
error
|
|
318
|
-
});
|
|
319
|
-
} else {
|
|
320
|
-
await driver.release({
|
|
321
|
-
id: queueMessage.id,
|
|
322
|
-
attempts: queueMessage.attempts,
|
|
323
|
-
error,
|
|
324
|
-
delaySeconds: e instanceof ReleaseQueueMessageError ? e.delaySeconds ?? getDefaultDelaySeconds({ queueConfig, queueMessage }) : getDefaultDelaySeconds({ queueConfig, queueMessage })
|
|
325
|
-
});
|
|
326
|
-
logger.warn({
|
|
327
|
-
event: "messageReleased",
|
|
328
|
-
queue: queueMessage.queue,
|
|
329
|
-
body: queueMessage.body,
|
|
330
|
-
messageId: queueMessage.id,
|
|
331
|
-
error,
|
|
332
|
-
attempt: queueMessage.attempts + 1
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
throw e;
|
|
336
410
|
}
|
|
337
411
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
function work({ maxJobs, maxTimeSeconds, stopWhenEmpty, sleepSeconds, concurrency = 1 }) {
|
|
341
|
-
const workAbortController = new AbortController();
|
|
342
|
-
let messagesProcessed = 0;
|
|
343
|
-
let messagesPopping = 0;
|
|
344
|
-
let timeoutToClear = null;
|
|
345
|
-
if (maxTimeSeconds) {
|
|
346
|
-
timeoutToClear = setTimeout(() => workAbortController.abort("max time exceeded"), maxTimeSeconds * 1e3);
|
|
412
|
+
async function listMessages({ pageSize, pageNumber }) {
|
|
413
|
+
return config.driver.listMessages({ pageSize, pageNumber, queueName });
|
|
347
414
|
}
|
|
348
|
-
async function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (typeof maxJobs === "number" && messagesProcessed + messagesPopping >= maxJobs) {
|
|
360
|
-
await new Promise((r) => setTimeout(r, 1e3 * (sleepSeconds || 1)));
|
|
361
|
-
} else {
|
|
362
|
-
messagesPopping++;
|
|
363
|
-
const message = await driver.pop().catch((e) => {
|
|
364
|
-
didThrowDuringPop = true;
|
|
365
|
-
messagesPopping--;
|
|
366
|
-
throw e;
|
|
367
|
-
});
|
|
368
|
-
messagesPopping--;
|
|
369
|
-
if (!message && stopWhenEmpty) {
|
|
370
|
-
workAbortController.abort("queue empty");
|
|
371
|
-
} else if (!message) {
|
|
372
|
-
await new Promise((r) => setTimeout(r, 1e3 * (sleepSeconds || 1)));
|
|
373
|
-
} else {
|
|
374
|
-
messagesProcessed++;
|
|
375
|
-
await processMessage(message);
|
|
415
|
+
async function addMessage(body, options2) {
|
|
416
|
+
const { id } = await tracing.hydrate(
|
|
417
|
+
options2.tracingContext === void 0 ? tracing.dehydrate() : options2.tracingContext,
|
|
418
|
+
async () => {
|
|
419
|
+
return tracing.startActiveSpan(
|
|
420
|
+
{
|
|
421
|
+
name: "queue_producer",
|
|
422
|
+
attributes: {
|
|
423
|
+
op: "queue.publish",
|
|
424
|
+
"sentry.op": "queue.publish",
|
|
425
|
+
"messaging.destination.name": queueName
|
|
376
426
|
}
|
|
427
|
+
},
|
|
428
|
+
async () => {
|
|
429
|
+
const span = tracing.getActiveSpan();
|
|
430
|
+
const { id: id2 } = await config.driver.addMessage({
|
|
431
|
+
body,
|
|
432
|
+
scheduledDate: options2.scheduledDate,
|
|
433
|
+
queueName,
|
|
434
|
+
traceData: tracing.dehydrate()
|
|
435
|
+
});
|
|
436
|
+
span?.setAttribute("messaging.message.id", id2);
|
|
437
|
+
return { id: id2 };
|
|
377
438
|
}
|
|
378
|
-
|
|
379
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
380
|
-
} catch (e) {
|
|
439
|
+
);
|
|
381
440
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
workPromises.push(result);
|
|
393
|
-
result.finally(() => {
|
|
394
|
-
workPromises = workPromises.filter((w) => w !== result);
|
|
395
|
-
if (timeoutToClear) {
|
|
396
|
-
clearTimeout(timeoutToClear);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
return result;
|
|
400
|
-
}
|
|
401
|
-
async function stopAllWork() {
|
|
402
|
-
const current = [...workPromises];
|
|
403
|
-
current.forEach((w) => w.abort());
|
|
404
|
-
await Promise.all(current);
|
|
405
|
-
}
|
|
406
|
-
function create(queueConfig) {
|
|
407
|
-
if (allQueues.has(queueConfig.name)) {
|
|
408
|
-
throw new Error(`Cannot have multiple queues with name "${queueConfig.name}"`);
|
|
441
|
+
);
|
|
442
|
+
logger.info({
|
|
443
|
+
event: "messageEnqueued",
|
|
444
|
+
queue: queueName,
|
|
445
|
+
body,
|
|
446
|
+
scheduledDate: options2.scheduledDate ?? null,
|
|
447
|
+
driver: config.driver,
|
|
448
|
+
messageId: id
|
|
449
|
+
});
|
|
409
450
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
451
|
+
async function updateMessage({
|
|
452
|
+
messageId,
|
|
453
|
+
scheduledDate,
|
|
454
|
+
body
|
|
455
|
+
}) {
|
|
456
|
+
return config.driver.updateMessage({ messageId, body, scheduledDate, queueName });
|
|
415
457
|
}
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
458
|
+
function processUntilEmpty(options2) {
|
|
459
|
+
const interval = setInterval(() => {
|
|
460
|
+
config.driver.isQueueEmpty({ queueName }).then((isEmpty2) => {
|
|
461
|
+
if (isEmpty2) {
|
|
462
|
+
declareEmpty(null);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}, options2?.pollingMs ?? 3e3);
|
|
466
|
+
let declareEmpty = (value) => {
|
|
467
|
+
};
|
|
468
|
+
const isEmpty = new Promise((resolve) => {
|
|
469
|
+
declareEmpty = resolve;
|
|
470
|
+
}).then(() => clearInterval(interval)).then(() => {
|
|
471
|
+
return stopProcessing2();
|
|
472
|
+
});
|
|
473
|
+
startProcessing2();
|
|
474
|
+
return isEmpty;
|
|
421
475
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
476
|
+
function startProcessing2() {
|
|
477
|
+
config.driver.start({ queueName, workFn, concurrency });
|
|
478
|
+
logger.info({
|
|
479
|
+
event: "queueStarted",
|
|
480
|
+
queue: queueName,
|
|
481
|
+
driver: config.driver
|
|
482
|
+
});
|
|
428
483
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
);
|
|
437
|
-
for (const { body } of messages) {
|
|
438
|
-
logger.info({ event: "messageEnqueued", queue: queueConfig.name, body });
|
|
439
|
-
}
|
|
484
|
+
function stopProcessing2() {
|
|
485
|
+
config.driver.stop({ queueName });
|
|
486
|
+
logger.info({
|
|
487
|
+
event: "queueStopped",
|
|
488
|
+
queue: queueName,
|
|
489
|
+
driver: config.driver
|
|
490
|
+
});
|
|
440
491
|
}
|
|
441
|
-
|
|
492
|
+
const queue = {
|
|
493
|
+
listMessages,
|
|
494
|
+
addMessage,
|
|
495
|
+
updateMessage,
|
|
496
|
+
startProcessing: startProcessing2,
|
|
497
|
+
processUntilEmpty,
|
|
498
|
+
stopProcessing: stopProcessing2,
|
|
499
|
+
name: options.name
|
|
500
|
+
};
|
|
501
|
+
queues.push(queue);
|
|
502
|
+
return queue;
|
|
442
503
|
}
|
|
443
|
-
|
|
504
|
+
function startProcessing() {
|
|
505
|
+
queues.forEach((q) => q.startProcessing());
|
|
506
|
+
}
|
|
507
|
+
function stopProcessing() {
|
|
508
|
+
queues.forEach((q) => q.stopProcessing());
|
|
509
|
+
}
|
|
510
|
+
return { list, get, create, startProcessing, stopProcessing };
|
|
444
511
|
}
|
|
445
512
|
var registry = ServiceRegistry.createRegistry(
|
|
446
513
|
ServiceRegistry.proxyFunctionsForObject
|
|
447
514
|
);
|
|
448
515
|
var Provider = {
|
|
449
516
|
create: createService,
|
|
450
|
-
register: registry.register
|
|
517
|
+
register: registry.register,
|
|
518
|
+
list: registry.list
|
|
451
519
|
};
|
|
452
520
|
function provider(key) {
|
|
453
521
|
const service = registry.resolve(key);
|
|
454
522
|
return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
523
|
+
list: service.list,
|
|
524
|
+
get: service.get,
|
|
525
|
+
create: service.create,
|
|
526
|
+
startProcessing: service.startProcessing,
|
|
527
|
+
stopProcessing: service.stopProcessing
|
|
459
528
|
};
|
|
460
529
|
}
|
|
461
530
|
var facade = provider("default");
|
|
462
|
-
var FailQueueMessageError = class extends Error {
|
|
463
|
-
cause;
|
|
464
|
-
constructor(cause) {
|
|
465
|
-
super(void 0, { cause });
|
|
466
|
-
this.cause = cause;
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
var ReleaseQueueMessageError = class extends Error {
|
|
470
|
-
cause;
|
|
471
|
-
delaySeconds = void 0;
|
|
472
|
-
constructor(cause, delaySeconds) {
|
|
473
|
-
super(void 0, { cause });
|
|
474
|
-
this.delaySeconds = delaySeconds;
|
|
475
|
-
this.cause = cause;
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
531
|
var Queue = {
|
|
532
|
+
drivers: {
|
|
533
|
+
db: DbQueueDriver
|
|
534
|
+
},
|
|
479
535
|
Provider,
|
|
480
536
|
provider,
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
processMessage: facade.processMessage,
|
|
537
|
+
list: facade.list,
|
|
538
|
+
get: facade.get,
|
|
484
539
|
create: facade.create,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
local: localQueueDriver
|
|
488
|
-
}
|
|
540
|
+
startProcessing: facade.startProcessing,
|
|
541
|
+
stopProcessing: facade.stopProcessing
|
|
489
542
|
};
|
|
490
543
|
export {
|
|
491
544
|
Queue
|