@getstrata/core 1.1.9 → 2.0.1
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/CHANGELOG.md +19 -0
- package/dist/core/database/transaction.d.ts +2 -1
- package/dist/core/database/transactionControl.d.ts +5 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
- package/dist/core/http/metricsMiddleware.d.ts +1 -2
- package/dist/core/http/requestMetaContext.d.ts +2 -0
- package/dist/core/http/throttleMiddleware.d.ts +11 -2
- package/dist/core/metrics/prometheus.d.ts +11 -1
- package/dist/core/queue/jobRunner.d.ts +3 -1
- package/dist/core/queue/redisQueue.d.ts +20 -3
- package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
- package/dist/entries/audit/exportAuditLogs.js +522 -62
- package/dist/entries/auth/basicAuthGuard.js +245 -219
- package/dist/entries/auth/oauth/samlProvider.js +185 -161
- package/dist/entries/auth/oneTimeToken.js +130 -127
- package/dist/entries/auth/passwordLogin.js +185 -180
- package/dist/entries/auth/saml/samlServiceProvider.js +185 -161
- package/dist/entries/auth/scimAuthMiddleware.js +525 -11
- package/dist/entries/database/migrations.js +132 -129
- package/dist/entries/database/schema.js +58 -39
- package/dist/entries/http/metricsMiddleware.js +124 -27
- package/dist/entries/http/response.js +107 -0
- package/dist/entries/http/routeMiddleware.js +40 -0
- package/dist/entries/http/throttleMiddleware.js +124 -11
- package/dist/entries/jobs/exportAuditLogsJob.js +522 -62
- package/dist/entries/metrics/prometheus.js +71 -11
- package/dist/entries/queue/createAppQueue.js +215 -21
- package/dist/entries/queue/jobRunner.js +3 -2
- package/dist/entries/queue/publicQueue.js +215 -21
- package/dist/entries/queue/queueMetrics.js +215 -21
- package/dist/entries/queue/redisQueue.js +223 -22
- package/dist/entries/tenant/databaseTenantContext.js +477 -16
- package/dist/entries/tenant/tenantDatabaseScope.js +523 -9
- package/dist/framework/public-api.d.ts +2 -2
- package/dist/index.js +954 -457
- package/package.json +1 -1
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/metrics/prometheus.ts
|
|
3
|
+
var HTTP_DURATION_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 1e4];
|
|
4
|
+
function emptyHistogram() {
|
|
5
|
+
return {
|
|
6
|
+
cumulative: HTTP_DURATION_BUCKETS_MS.map(() => 0),
|
|
7
|
+
sum: 0,
|
|
8
|
+
count: 0
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
class PrometheusRegistry {
|
|
13
|
+
maxSeries;
|
|
14
|
+
seriesLabels = new Map;
|
|
15
|
+
constructor(maxSeries = 4096) {
|
|
16
|
+
this.maxSeries = maxSeries;
|
|
17
|
+
if (!Number.isSafeInteger(maxSeries) || maxSeries < 1) {
|
|
18
|
+
throw new Error("Metrics maxSeries must be a positive safe integer.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
4
21
|
httpRequestsTotal = new Map;
|
|
5
22
|
httpRequestDurationMs = new Map;
|
|
6
23
|
incrementHttpRequest(labels) {
|
|
@@ -8,10 +25,20 @@ class PrometheusRegistry {
|
|
|
8
25
|
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
9
26
|
}
|
|
10
27
|
observeHttpDuration(labels, durationMs) {
|
|
28
|
+
if (!Number.isFinite(durationMs) || durationMs < 0)
|
|
29
|
+
return;
|
|
11
30
|
const key = this.metricKey(labels);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
const histogram = this.httpRequestDurationMs.get(key) ?? emptyHistogram();
|
|
32
|
+
histogram.count += 1;
|
|
33
|
+
histogram.sum += durationMs;
|
|
34
|
+
for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
|
|
35
|
+
const bound = HTTP_DURATION_BUCKETS_MS[index];
|
|
36
|
+
if (bound !== undefined && durationMs <= bound) {
|
|
37
|
+
const count = histogram.cumulative[index] ?? 0;
|
|
38
|
+
histogram.cumulative[index] = count + 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.httpRequestDurationMs.set(key, histogram);
|
|
15
42
|
}
|
|
16
43
|
renderMetrics() {
|
|
17
44
|
const lines = [
|
|
@@ -21,16 +48,22 @@ class PrometheusRegistry {
|
|
|
21
48
|
for (const [key, value] of this.httpRequestsTotal) {
|
|
22
49
|
lines.push(`http_requests_total{${key}} ${value}`);
|
|
23
50
|
}
|
|
24
|
-
lines.push("# HELP
|
|
25
|
-
for (const [key,
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
lines.push("# HELP http_request_duration_ms HTTP request duration in milliseconds.", "# TYPE http_request_duration_ms histogram");
|
|
52
|
+
for (const [key, histogram] of this.httpRequestDurationMs) {
|
|
53
|
+
for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
|
|
54
|
+
const bound = HTTP_DURATION_BUCKETS_MS[index];
|
|
55
|
+
lines.push(`http_request_duration_ms_bucket{${key},le="${bound}"} ${histogram.cumulative[index] ?? 0}`);
|
|
56
|
+
}
|
|
57
|
+
lines.push(`http_request_duration_ms_bucket{${key},le="+Inf"} ${histogram.count}`);
|
|
58
|
+
lines.push(`http_request_duration_ms_sum{${key}} ${histogram.sum}`);
|
|
59
|
+
lines.push(`http_request_duration_ms_count{${key}} ${histogram.count}`);
|
|
28
60
|
}
|
|
29
61
|
return `${lines.join(`
|
|
30
62
|
`)}
|
|
31
63
|
`;
|
|
32
64
|
}
|
|
33
65
|
resetForTests() {
|
|
66
|
+
this.seriesLabels.clear();
|
|
34
67
|
this.httpRequestsTotal.clear();
|
|
35
68
|
this.httpRequestDurationMs.clear();
|
|
36
69
|
}
|
|
@@ -40,9 +73,10 @@ class PrometheusRegistry {
|
|
|
40
73
|
let totalRequests = 0;
|
|
41
74
|
for (const [key, count] of this.httpRequestsTotal) {
|
|
42
75
|
totalRequests += count;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
76
|
+
const labels = this.seriesLabels.get(key);
|
|
77
|
+
if (!labels)
|
|
78
|
+
continue;
|
|
79
|
+
const { method, path, status } = labels;
|
|
46
80
|
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
47
81
|
const pathKey = `${method} ${path}`;
|
|
48
82
|
const existing = pathCounts.get(pathKey);
|
|
@@ -59,12 +93,38 @@ class PrometheusRegistry {
|
|
|
59
93
|
topPaths
|
|
60
94
|
};
|
|
61
95
|
}
|
|
96
|
+
getStorageStats() {
|
|
97
|
+
return {
|
|
98
|
+
series: this.seriesLabels.size,
|
|
99
|
+
histograms: this.httpRequestDurationMs.size,
|
|
100
|
+
bucketCounters: this.httpRequestDurationMs.size * HTTP_DURATION_BUCKETS_MS.length
|
|
101
|
+
};
|
|
102
|
+
}
|
|
62
103
|
metricKey(labels) {
|
|
63
|
-
|
|
104
|
+
const normalized = {
|
|
105
|
+
method: /^(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)$/.test(labels.method) ? labels.method : "OTHER",
|
|
106
|
+
path: labels.path.length <= 512 ? labels.path : "__overflow__",
|
|
107
|
+
status: /^[1-5][0-9]{2}$/.test(labels.status) ? labels.status : "000"
|
|
108
|
+
};
|
|
109
|
+
const escapeLabel = (value) => value.replaceAll("\\", "\\\\").replaceAll(`
|
|
110
|
+
`, "\\n").replaceAll('"', "\\\"");
|
|
111
|
+
const encode = (value) => `method="${value.method}",path="${escapeLabel(value.path)}",status="${value.status}"`;
|
|
112
|
+
let key = encode(normalized);
|
|
113
|
+
if (!this.seriesLabels.has(key)) {
|
|
114
|
+
if (this.seriesLabels.size >= this.maxSeries) {
|
|
115
|
+
const overflow = { method: "OTHER", path: "__overflow__", status: "000" };
|
|
116
|
+
key = encode(overflow);
|
|
117
|
+
this.seriesLabels.set(key, overflow);
|
|
118
|
+
} else {
|
|
119
|
+
this.seriesLabels.set(key, normalized);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return key;
|
|
64
123
|
}
|
|
65
124
|
}
|
|
66
125
|
var prometheusRegistry = new PrometheusRegistry;
|
|
67
126
|
export {
|
|
127
|
+
HTTP_DURATION_BUCKETS_MS,
|
|
68
128
|
PrometheusRegistry,
|
|
69
129
|
prometheusRegistry
|
|
70
130
|
};
|
|
@@ -124,7 +124,7 @@ var queueConfig = {
|
|
|
124
124
|
};
|
|
125
125
|
|
|
126
126
|
// ../../src/core/queue/jobRunner.ts
|
|
127
|
-
async function runQueueJob(envelope, failedJobs) {
|
|
127
|
+
async function runQueueJob(envelope, failedJobs, options = {}) {
|
|
128
128
|
const job = jobRegistry.create(envelope.name);
|
|
129
129
|
if (!job) {
|
|
130
130
|
throw new Error(`Unknown job "${envelope.name}".`);
|
|
@@ -141,7 +141,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
141
141
|
await runQueueJob({
|
|
142
142
|
...envelope,
|
|
143
143
|
attempts: nextAttempt
|
|
144
|
-
}, failedJobs);
|
|
144
|
+
}, failedJobs, options);
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
await failedJobs.recordFailure({
|
|
@@ -149,6 +149,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
149
149
|
payload: envelope.payload,
|
|
150
150
|
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
151
151
|
});
|
|
152
|
+
options.onFailureRecorded?.();
|
|
152
153
|
throw error;
|
|
153
154
|
}
|
|
154
155
|
}
|
|
@@ -256,6 +257,30 @@ var QUEUE_LIST_KEY = queueListKey();
|
|
|
256
257
|
var QUEUE_HIGH_KEY = queueHighKey();
|
|
257
258
|
var QUEUE_LOW_KEY = queueLowKey();
|
|
258
259
|
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
260
|
+
var DEFAULT_VISIBILITY_MS = 60000;
|
|
261
|
+
function queueProcessingKey(queueKey) {
|
|
262
|
+
return `${queueKey}:processing`;
|
|
263
|
+
}
|
|
264
|
+
function queueProcessingLeaseKey(queueKey) {
|
|
265
|
+
return `${queueKey}:processing:leases`;
|
|
266
|
+
}
|
|
267
|
+
function readVisibilityMs() {
|
|
268
|
+
const parsed = Number(process.env.QUEUE_VISIBILITY_MS ?? String(DEFAULT_VISIBILITY_MS));
|
|
269
|
+
if (!Number.isSafeInteger(parsed) || parsed < 30) {
|
|
270
|
+
throw new Error("QUEUE_VISIBILITY_MS must be an integer of at least 30 milliseconds.");
|
|
271
|
+
}
|
|
272
|
+
return parsed;
|
|
273
|
+
}
|
|
274
|
+
function reservationExpired(lease, now, visibilityMs) {
|
|
275
|
+
if (lease === null || lease.length === 0) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
const reservedAt = Number(lease);
|
|
279
|
+
if (!Number.isFinite(reservedAt)) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return now - reservedAt >= visibilityMs;
|
|
283
|
+
}
|
|
259
284
|
function queueKeyForPriority(priority = "default") {
|
|
260
285
|
switch (priority) {
|
|
261
286
|
case "high":
|
|
@@ -297,6 +322,129 @@ function parseQueueJobEnvelope(rawPayload) {
|
|
|
297
322
|
attempts: typeof envelope.attempts === "number" ? envelope.attempts : 0
|
|
298
323
|
};
|
|
299
324
|
}
|
|
325
|
+
var SCRIPT_HEADER = `
|
|
326
|
+
local function checkType(key, expected)
|
|
327
|
+
local actual = redis.call('TYPE', key).ok
|
|
328
|
+
if actual ~= 'none' and actual ~= expected then
|
|
329
|
+
return redis.error_reply('Unexpected queue key type: ' .. key)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
local function nowMs()
|
|
333
|
+
local time = redis.call('TIME')
|
|
334
|
+
return tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
|
|
335
|
+
end
|
|
336
|
+
`;
|
|
337
|
+
var RESERVE_JOB = `${SCRIPT_HEADER}
|
|
338
|
+
local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'list') or checkType(KEYS[3], 'hash')
|
|
339
|
+
if err then return err end
|
|
340
|
+
local payload = redis.call('LINDEX', KEYS[1], -1)
|
|
341
|
+
if not payload then return nil end
|
|
342
|
+
local record = cjson.encode({strataReservation=1, id=ARGV[1], owner=ARGV[2], payload=payload})
|
|
343
|
+
local lease = cjson.encode({owner=ARGV[2], expiresAt=nowMs()+tonumber(ARGV[3])})
|
|
344
|
+
redis.call('LPUSH', KEYS[2], record)
|
|
345
|
+
redis.call('HSET', KEYS[3], ARGV[1], lease)
|
|
346
|
+
redis.call('RPOP', KEYS[1])
|
|
347
|
+
return record
|
|
348
|
+
`;
|
|
349
|
+
var RENEW_OR_ACK = `${SCRIPT_HEADER}
|
|
350
|
+
local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'hash') or checkType(KEYS[3], 'list')
|
|
351
|
+
if err then return err end
|
|
352
|
+
local raw = redis.call('HGET', KEYS[2], ARGV[1])
|
|
353
|
+
if not raw then return 0 end
|
|
354
|
+
local ok, lease = pcall(cjson.decode, raw)
|
|
355
|
+
local now = nowMs()
|
|
356
|
+
if not ok or type(lease) ~= 'table' or lease.owner ~= ARGV[2] or not tonumber(lease.expiresAt) or tonumber(lease.expiresAt) <= now then
|
|
357
|
+
return 0
|
|
358
|
+
end
|
|
359
|
+
if ARGV[4] == 'renew' then
|
|
360
|
+
redis.call('HSET', KEYS[2], ARGV[1], cjson.encode({owner=ARGV[2], expiresAt=now+tonumber(ARGV[5])}))
|
|
361
|
+
return 1
|
|
362
|
+
end
|
|
363
|
+
if ARGV[4] == 'quarantine' then redis.call('LPUSH', KEYS[3], ARGV[6]) end
|
|
364
|
+
local removed = redis.call('LREM', KEYS[1], 1, ARGV[3])
|
|
365
|
+
redis.call('HDEL', KEYS[2], ARGV[1])
|
|
366
|
+
return removed
|
|
367
|
+
`;
|
|
368
|
+
var RECLAIM_JOBS = `${SCRIPT_HEADER}
|
|
369
|
+
local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'list') or checkType(KEYS[3], 'hash')
|
|
370
|
+
if err then return err end
|
|
371
|
+
local now = nowMs()
|
|
372
|
+
local reclaimed = 0
|
|
373
|
+
for _, record in ipairs(redis.call('LRANGE', KEYS[2], 0, -1)) do
|
|
374
|
+
local payload, field = record, record
|
|
375
|
+
local modern = false
|
|
376
|
+
local ok, reservation = pcall(cjson.decode, record)
|
|
377
|
+
if ok and type(reservation) == 'table' and reservation.strataReservation == 1 then
|
|
378
|
+
payload, field, modern = reservation.payload, reservation.id, true
|
|
379
|
+
end
|
|
380
|
+
local raw = redis.call('HGET', KEYS[3], field)
|
|
381
|
+
local expired = true
|
|
382
|
+
if raw then
|
|
383
|
+
if modern then
|
|
384
|
+
local leaseOk, lease = pcall(cjson.decode, raw)
|
|
385
|
+
expired = not leaseOk or type(lease) ~= 'table' or not tonumber(lease.expiresAt) or tonumber(lease.expiresAt) <= now
|
|
386
|
+
else
|
|
387
|
+
local reservedAt = tonumber(raw)
|
|
388
|
+
expired = not reservedAt or now - reservedAt >= tonumber(ARGV[1])
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
if expired then
|
|
392
|
+
redis.call('LPUSH', KEYS[1], payload)
|
|
393
|
+
redis.call('LREM', KEYS[2], 1, record)
|
|
394
|
+
redis.call('HDEL', KEYS[3], field)
|
|
395
|
+
reclaimed = reclaimed + 1
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
return reclaimed
|
|
399
|
+
`;
|
|
400
|
+
function queueInvalidKey(queueKey) {
|
|
401
|
+
return `${queueKey}:invalid`;
|
|
402
|
+
}
|
|
403
|
+
async function reserveQueueJob(client, queueKey, owner) {
|
|
404
|
+
const raw = await client.send("EVAL", [
|
|
405
|
+
RESERVE_JOB,
|
|
406
|
+
"3",
|
|
407
|
+
queueKey,
|
|
408
|
+
queueProcessingKey(queueKey),
|
|
409
|
+
queueProcessingLeaseKey(queueKey),
|
|
410
|
+
crypto.randomUUID(),
|
|
411
|
+
owner,
|
|
412
|
+
String(readVisibilityMs())
|
|
413
|
+
]);
|
|
414
|
+
if (typeof raw !== "string")
|
|
415
|
+
return null;
|
|
416
|
+
const reservation = JSON.parse(raw);
|
|
417
|
+
return { ...reservation, record: raw };
|
|
418
|
+
}
|
|
419
|
+
async function updateQueueReservation(client, queueKey, reservation, action) {
|
|
420
|
+
return Number(await client.send("EVAL", [
|
|
421
|
+
RENEW_OR_ACK,
|
|
422
|
+
"3",
|
|
423
|
+
queueProcessingKey(queueKey),
|
|
424
|
+
queueProcessingLeaseKey(queueKey),
|
|
425
|
+
queueInvalidKey(queueKey),
|
|
426
|
+
reservation.id,
|
|
427
|
+
reservation.owner,
|
|
428
|
+
reservation.record,
|
|
429
|
+
action,
|
|
430
|
+
String(readVisibilityMs()),
|
|
431
|
+
reservation.payload
|
|
432
|
+
])) === 1;
|
|
433
|
+
}
|
|
434
|
+
async function reclaimExpiredQueueReservations(client, queueKeys = QUEUE_KEYS) {
|
|
435
|
+
let reclaimed = 0;
|
|
436
|
+
for (const key of queueKeys) {
|
|
437
|
+
reclaimed += Number(await client.send("EVAL", [
|
|
438
|
+
RECLAIM_JOBS,
|
|
439
|
+
"3",
|
|
440
|
+
key,
|
|
441
|
+
queueProcessingKey(key),
|
|
442
|
+
queueProcessingLeaseKey(key),
|
|
443
|
+
String(readVisibilityMs())
|
|
444
|
+
]));
|
|
445
|
+
}
|
|
446
|
+
return reclaimed;
|
|
447
|
+
}
|
|
300
448
|
|
|
301
449
|
class RedisQueue {
|
|
302
450
|
client;
|
|
@@ -321,12 +469,15 @@ class RedisQueue {
|
|
|
321
469
|
class QueueWorker {
|
|
322
470
|
failedJobs;
|
|
323
471
|
timeoutSeconds;
|
|
472
|
+
queueKeys;
|
|
473
|
+
owner = crypto.randomUUID();
|
|
324
474
|
running = false;
|
|
325
475
|
stopping = false;
|
|
326
476
|
client;
|
|
327
|
-
constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
|
|
477
|
+
constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
|
|
328
478
|
this.failedJobs = failedJobs;
|
|
329
479
|
this.timeoutSeconds = timeoutSeconds;
|
|
480
|
+
this.queueKeys = queueKeys;
|
|
330
481
|
this.client = new RedisClient(redisUrl);
|
|
331
482
|
}
|
|
332
483
|
requestStop() {
|
|
@@ -335,38 +486,81 @@ class QueueWorker {
|
|
|
335
486
|
isRunning() {
|
|
336
487
|
return this.running;
|
|
337
488
|
}
|
|
338
|
-
async
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
489
|
+
async reserve() {
|
|
490
|
+
const deadline = performance.now() + this.timeoutSeconds * 1000;
|
|
491
|
+
do {
|
|
492
|
+
for (const key of this.queueKeys) {
|
|
493
|
+
if (this.stopping)
|
|
494
|
+
return null;
|
|
495
|
+
const reservation = await reserveQueueJob(this.client, key, this.owner);
|
|
496
|
+
if (reservation)
|
|
497
|
+
return { queueKey: key, reservation };
|
|
344
498
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
499
|
+
if (this.stopping || performance.now() >= deadline)
|
|
500
|
+
return null;
|
|
501
|
+
await Bun.sleep(Math.min(100, Math.max(1, deadline - performance.now())));
|
|
502
|
+
} while (!this.stopping);
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
async processNext() {
|
|
506
|
+
if (this.stopping)
|
|
350
507
|
return false;
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
508
|
+
await reclaimExpiredQueueReservations(this.client, this.queueKeys);
|
|
509
|
+
const reserved = await this.reserve();
|
|
510
|
+
if (!reserved)
|
|
511
|
+
return false;
|
|
512
|
+
const { queueKey, reservation } = reserved;
|
|
513
|
+
const envelope = parseQueueJobEnvelope(reservation.payload);
|
|
354
514
|
if (!envelope) {
|
|
515
|
+
await updateQueueReservation(this.client, queueKey, reservation, "quarantine");
|
|
355
516
|
return true;
|
|
356
517
|
}
|
|
518
|
+
let acknowledge = false;
|
|
519
|
+
let ownershipLost = false;
|
|
520
|
+
let renewal;
|
|
521
|
+
const heartbeat = setInterval(() => {
|
|
522
|
+
if (renewal || ownershipLost)
|
|
523
|
+
return;
|
|
524
|
+
renewal = (async () => {
|
|
525
|
+
try {
|
|
526
|
+
if (!await updateQueueReservation(this.client, queueKey, reservation, "renew")) {
|
|
527
|
+
ownershipLost = true;
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
ownershipLost = true;
|
|
531
|
+
console.error("[QueueWorker] Lease renewal failed:", error);
|
|
532
|
+
}
|
|
533
|
+
})().finally(() => {
|
|
534
|
+
renewal = undefined;
|
|
535
|
+
});
|
|
536
|
+
}, Math.max(1, Math.floor(readVisibilityMs() / 3)));
|
|
357
537
|
try {
|
|
358
|
-
await runQueueJob(envelope, this.failedJobs
|
|
538
|
+
await runQueueJob(envelope, this.failedJobs, {
|
|
539
|
+
onFailureRecorded: () => {
|
|
540
|
+
acknowledge = true;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
acknowledge = true;
|
|
359
544
|
} catch (error) {
|
|
360
545
|
console.error("[QueueWorker] Job failed:", error);
|
|
546
|
+
} finally {
|
|
547
|
+
clearInterval(heartbeat);
|
|
548
|
+
await renewal;
|
|
549
|
+
}
|
|
550
|
+
if (acknowledge && !ownershipLost) {
|
|
551
|
+
await updateQueueReservation(this.client, queueKey, reservation, "ack");
|
|
361
552
|
}
|
|
362
553
|
return true;
|
|
363
554
|
}
|
|
364
555
|
async run() {
|
|
365
556
|
this.running = true;
|
|
366
|
-
|
|
367
|
-
|
|
557
|
+
try {
|
|
558
|
+
while (!this.stopping) {
|
|
559
|
+
await this.processNext();
|
|
560
|
+
}
|
|
561
|
+
} finally {
|
|
562
|
+
this.running = false;
|
|
368
563
|
}
|
|
369
|
-
this.running = false;
|
|
370
564
|
}
|
|
371
565
|
close() {
|
|
372
566
|
this.client.close();
|
|
@@ -60,7 +60,7 @@ var queueConfig = {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
// ../../src/core/queue/jobRunner.ts
|
|
63
|
-
async function runQueueJob(envelope, failedJobs) {
|
|
63
|
+
async function runQueueJob(envelope, failedJobs, options = {}) {
|
|
64
64
|
const job = jobRegistry.create(envelope.name);
|
|
65
65
|
if (!job) {
|
|
66
66
|
throw new Error(`Unknown job "${envelope.name}".`);
|
|
@@ -77,7 +77,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
77
77
|
await runQueueJob({
|
|
78
78
|
...envelope,
|
|
79
79
|
attempts: nextAttempt
|
|
80
|
-
}, failedJobs);
|
|
80
|
+
}, failedJobs, options);
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
await failedJobs.recordFailure({
|
|
@@ -85,6 +85,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
85
85
|
payload: envelope.payload,
|
|
86
86
|
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
87
87
|
});
|
|
88
|
+
options.onFailureRecorded?.();
|
|
88
89
|
throw error;
|
|
89
90
|
}
|
|
90
91
|
}
|