@trigger.dev/redis-worker 4.5.0-rc.7 → 4.5.0
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/dist/index.cjs +109 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +109 -78
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -9992,6 +9992,37 @@ var SimpleQueue = class {
|
|
|
9992
9992
|
throw e;
|
|
9993
9993
|
}
|
|
9994
9994
|
}
|
|
9995
|
+
/**
|
|
9996
|
+
* Age (in ms) of the oldest *overdue* message — the oldest item whose scheduled
|
|
9997
|
+
* time has already passed (score <= now). Returns 0 when the queue is empty or
|
|
9998
|
+
* only holds future/delayed or in-flight (future-scored) items.
|
|
9999
|
+
*
|
|
10000
|
+
* Resolves the candidate against the `items` hash so orphaned `queue` entries
|
|
10001
|
+
* (a member whose payload is missing — the same stale state `dequeueItems`
|
|
10002
|
+
* cleans up) don't report a phantom stall for work that can't be dequeued. The
|
|
10003
|
+
* Lua scans due items oldest-first and returns the first score whose payload
|
|
10004
|
+
* still exists.
|
|
10005
|
+
*
|
|
10006
|
+
* This is the generic stall signal: it stays at 0 while a queue drains healthily
|
|
10007
|
+
* and rises only when due work sits undrained (poison block, dead consumer,
|
|
10008
|
+
* backpressure).
|
|
10009
|
+
*/
|
|
10010
|
+
async oldestMessageAge() {
|
|
10011
|
+
try {
|
|
10012
|
+
const now = Date.now();
|
|
10013
|
+
const score = Number(await this.redis.getOldestDueScore(`queue`, `items`, now));
|
|
10014
|
+
if (!Number.isFinite(score) || score < 0) {
|
|
10015
|
+
return 0;
|
|
10016
|
+
}
|
|
10017
|
+
return Math.max(0, now - score);
|
|
10018
|
+
} catch (e) {
|
|
10019
|
+
this.logger.error(`SimpleQueue ${this.name}.oldestMessageAge(): error getting oldest age`, {
|
|
10020
|
+
queue: this.name,
|
|
10021
|
+
error: e
|
|
10022
|
+
});
|
|
10023
|
+
return 0;
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
9995
10026
|
async getJob(id) {
|
|
9996
10027
|
const result = await this.redis.getJob(`queue`, `items`, id);
|
|
9997
10028
|
if (!result) {
|
|
@@ -10158,6 +10189,29 @@ var SimpleQueue = class {
|
|
|
10158
10189
|
return dequeued
|
|
10159
10190
|
`
|
|
10160
10191
|
});
|
|
10192
|
+
this.redis.defineCommand("getOldestDueScore", {
|
|
10193
|
+
numberOfKeys: 2,
|
|
10194
|
+
lua: `
|
|
10195
|
+
local queue = KEYS[1]
|
|
10196
|
+
local items = KEYS[2]
|
|
10197
|
+
local now = tonumber(ARGV[1])
|
|
10198
|
+
|
|
10199
|
+
-- Oldest-first scan of due items, bounded so a long prefix of orphans can't
|
|
10200
|
+
-- make this O(n). Orphans are rare (dequeueItems removes them), so in the
|
|
10201
|
+
-- common case this returns on the first iteration. Read-only: unlike
|
|
10202
|
+
-- dequeueItems we don't ZREM orphans here \u2014 a metric probe must not mutate.
|
|
10203
|
+
local result = redis.call('ZRANGEBYSCORE', queue, '-inf', now, 'WITHSCORES', 'LIMIT', 0, 100)
|
|
10204
|
+
|
|
10205
|
+
for i = 1, #result, 2 do
|
|
10206
|
+
local id = result[i]
|
|
10207
|
+
if redis.call('HEXISTS', items, id) == 1 then
|
|
10208
|
+
return result[i + 1]
|
|
10209
|
+
end
|
|
10210
|
+
end
|
|
10211
|
+
|
|
10212
|
+
return -1
|
|
10213
|
+
`
|
|
10214
|
+
});
|
|
10161
10215
|
this.redis.defineCommand("getJob", {
|
|
10162
10216
|
numberOfKeys: 2,
|
|
10163
10217
|
lua: `
|
|
@@ -11402,6 +11456,15 @@ var Worker = class _Worker {
|
|
|
11402
11456
|
concurrencyLimitPendingObservableGauge.addCallback(
|
|
11403
11457
|
this.#updateConcurrencyLimitPendingMetric.bind(this)
|
|
11404
11458
|
);
|
|
11459
|
+
const oldestMessageAgeObservableGauge = this.meter.createObservableGauge(
|
|
11460
|
+
"redis_worker.queue.oldest_message_age",
|
|
11461
|
+
{
|
|
11462
|
+
description: "Age of the oldest overdue message in the queue",
|
|
11463
|
+
unit: "ms",
|
|
11464
|
+
valueType: ValueType.INT
|
|
11465
|
+
}
|
|
11466
|
+
);
|
|
11467
|
+
oldestMessageAgeObservableGauge.addCallback(this.#updateOldestMessageAgeMetric.bind(this));
|
|
11405
11468
|
}
|
|
11406
11469
|
subscriber;
|
|
11407
11470
|
tracer;
|
|
@@ -11430,6 +11493,12 @@ var Worker = class _Worker {
|
|
|
11430
11493
|
worker_name: this.options.name
|
|
11431
11494
|
});
|
|
11432
11495
|
}
|
|
11496
|
+
async #updateOldestMessageAgeMetric(observableResult) {
|
|
11497
|
+
const oldestMessageAge = await this.queue.oldestMessageAge();
|
|
11498
|
+
observableResult.observe(oldestMessageAge, {
|
|
11499
|
+
worker_name: this.options.name
|
|
11500
|
+
});
|
|
11501
|
+
}
|
|
11433
11502
|
async #updateConcurrencyLimitActiveMetric(observableResult) {
|
|
11434
11503
|
for (const [workerId, limiter] of Object.entries(this.limiters)) {
|
|
11435
11504
|
observableResult.observe(limiter.activeCount, {
|
|
@@ -11723,15 +11792,15 @@ var Worker = class _Worker {
|
|
|
11723
11792
|
await this.flushBatch(queueItem.job, workerId, limiter);
|
|
11724
11793
|
}
|
|
11725
11794
|
} else {
|
|
11726
|
-
limiter(
|
|
11727
|
-
() =>
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
}
|
|
11734
|
-
|
|
11795
|
+
limiter(() => this.processItem(queueItem, items.length, workerId, limiter)).catch(
|
|
11796
|
+
(err) => {
|
|
11797
|
+
this.logger.error("Unhandled error in processItem:", {
|
|
11798
|
+
error: err,
|
|
11799
|
+
workerId,
|
|
11800
|
+
item
|
|
11801
|
+
});
|
|
11802
|
+
}
|
|
11803
|
+
);
|
|
11735
11804
|
}
|
|
11736
11805
|
}
|
|
11737
11806
|
} catch (error) {
|
|
@@ -11881,13 +11950,25 @@ var Worker = class _Worker {
|
|
|
11881
11950
|
attempt: newAttempt
|
|
11882
11951
|
};
|
|
11883
11952
|
if (!shouldLogError) {
|
|
11884
|
-
this.logger.info(
|
|
11953
|
+
this.logger.info(
|
|
11954
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11955
|
+
dlqLogAttributes
|
|
11956
|
+
);
|
|
11885
11957
|
} else if (errorLogLevel === "warn") {
|
|
11886
|
-
this.logger.warn(
|
|
11958
|
+
this.logger.warn(
|
|
11959
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11960
|
+
dlqLogAttributes
|
|
11961
|
+
);
|
|
11887
11962
|
} else if (errorLogLevel === "info") {
|
|
11888
|
-
this.logger.info(
|
|
11963
|
+
this.logger.info(
|
|
11964
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11965
|
+
dlqLogAttributes
|
|
11966
|
+
);
|
|
11889
11967
|
} else {
|
|
11890
|
-
this.logger.error(
|
|
11968
|
+
this.logger.error(
|
|
11969
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11970
|
+
dlqLogAttributes
|
|
11971
|
+
);
|
|
11891
11972
|
}
|
|
11892
11973
|
await this.queue.moveToDeadLetterQueue(item.id, errorMessage);
|
|
11893
11974
|
continue;
|
|
@@ -14396,9 +14477,7 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
14396
14477
|
};
|
|
14397
14478
|
})
|
|
14398
14479
|
);
|
|
14399
|
-
const eligibleTenants = tenantData.filter(
|
|
14400
|
-
(t) => !t.isAtCapacity && t.deficit >= 1
|
|
14401
|
-
);
|
|
14480
|
+
const eligibleTenants = tenantData.filter((t) => !t.isAtCapacity && t.deficit >= 1);
|
|
14402
14481
|
const blockedTenants = tenantData.filter((t) => t.isAtCapacity);
|
|
14403
14482
|
if (blockedTenants.length > 0) {
|
|
14404
14483
|
this.logger.debug("DRR: tenants blocked by concurrency", {
|
|
@@ -14691,11 +14770,7 @@ var WeightedScheduler = class extends BaseScheduler {
|
|
|
14691
14770
|
// FairScheduler Implementation
|
|
14692
14771
|
// ============================================================================
|
|
14693
14772
|
async selectQueues(masterQueueShard, consumerId, context2) {
|
|
14694
|
-
const snapshot = await this.#getOrCreateSnapshot(
|
|
14695
|
-
masterQueueShard,
|
|
14696
|
-
consumerId,
|
|
14697
|
-
context2
|
|
14698
|
-
);
|
|
14773
|
+
const snapshot = await this.#getOrCreateSnapshot(masterQueueShard, consumerId, context2);
|
|
14699
14774
|
if (snapshot.queues.length === 0) {
|
|
14700
14775
|
return [];
|
|
14701
14776
|
}
|
|
@@ -16068,10 +16143,7 @@ var FairQueue = class {
|
|
|
16068
16143
|
if (this.concurrencyManager && storedMessage) {
|
|
16069
16144
|
await this.concurrencyManager.release(descriptor, messageId);
|
|
16070
16145
|
}
|
|
16071
|
-
const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(
|
|
16072
|
-
queueId,
|
|
16073
|
-
descriptor.tenantId
|
|
16074
|
-
);
|
|
16146
|
+
const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId);
|
|
16075
16147
|
if (queueEmpty) {
|
|
16076
16148
|
this.queueDescriptorCache.delete(queueId);
|
|
16077
16149
|
this.queueCooloffStates.delete(queueId);
|
|
@@ -16328,38 +16400,6 @@ var FairQueue = class {
|
|
|
16328
16400
|
}
|
|
16329
16401
|
return false;
|
|
16330
16402
|
}
|
|
16331
|
-
#incrementCooloff(queueId) {
|
|
16332
|
-
if (this.queueCooloffStates.size >= this.maxCooloffStatesSize) {
|
|
16333
|
-
this.logger.warn("Cooloff states cache hit size cap, clearing all entries", {
|
|
16334
|
-
size: this.queueCooloffStates.size,
|
|
16335
|
-
cap: this.maxCooloffStatesSize
|
|
16336
|
-
});
|
|
16337
|
-
this.queueCooloffStates.clear();
|
|
16338
|
-
}
|
|
16339
|
-
const state = this.queueCooloffStates.get(queueId) ?? {
|
|
16340
|
-
tag: "normal",
|
|
16341
|
-
consecutiveFailures: 0
|
|
16342
|
-
};
|
|
16343
|
-
if (state.tag === "normal") {
|
|
16344
|
-
const newFailures = state.consecutiveFailures + 1;
|
|
16345
|
-
if (newFailures >= this.cooloffThreshold) {
|
|
16346
|
-
this.queueCooloffStates.set(queueId, {
|
|
16347
|
-
tag: "cooloff",
|
|
16348
|
-
expiresAt: Date.now() + this.cooloffPeriodMs
|
|
16349
|
-
});
|
|
16350
|
-
this.logger.debug("Queue entered cooloff", {
|
|
16351
|
-
queueId,
|
|
16352
|
-
cooloffPeriodMs: this.cooloffPeriodMs,
|
|
16353
|
-
consecutiveFailures: newFailures
|
|
16354
|
-
});
|
|
16355
|
-
} else {
|
|
16356
|
-
this.queueCooloffStates.set(queueId, {
|
|
16357
|
-
tag: "normal",
|
|
16358
|
-
consecutiveFailures: newFailures
|
|
16359
|
-
});
|
|
16360
|
-
}
|
|
16361
|
-
}
|
|
16362
|
-
}
|
|
16363
16403
|
#resetCooloff(queueId) {
|
|
16364
16404
|
this.queueCooloffStates.delete(queueId);
|
|
16365
16405
|
}
|
|
@@ -16627,7 +16667,10 @@ var stringToError = zod.z.string().transform((v, ctx) => {
|
|
|
16627
16667
|
try {
|
|
16628
16668
|
return BufferEntryError.parse(JSON.parse(v));
|
|
16629
16669
|
} catch {
|
|
16630
|
-
ctx.addIssue({
|
|
16670
|
+
ctx.addIssue({
|
|
16671
|
+
code: zod.z.ZodIssueCode.custom,
|
|
16672
|
+
message: "expected JSON-encoded BufferEntryError"
|
|
16673
|
+
});
|
|
16631
16674
|
return zod.z.NEVER;
|
|
16632
16675
|
}
|
|
16633
16676
|
});
|
|
@@ -16825,11 +16868,7 @@ var MollifierBuffer = class {
|
|
|
16825
16868
|
// not an error.
|
|
16826
16869
|
async listEntriesForEnv(envId, maxCount) {
|
|
16827
16870
|
if (maxCount <= 0) return [];
|
|
16828
|
-
const runIds = await this.redis.lrange(
|
|
16829
|
-
`mollifier:queue:${envId}`,
|
|
16830
|
-
0,
|
|
16831
|
-
maxCount - 1
|
|
16832
|
-
);
|
|
16871
|
+
const runIds = await this.redis.lrange(`mollifier:queue:${envId}`, 0, maxCount - 1);
|
|
16833
16872
|
if (runIds.length === 0) return [];
|
|
16834
16873
|
const pipeline = this.redis.pipeline();
|
|
16835
16874
|
for (const runId of runIds) {
|
|
@@ -16966,10 +17005,7 @@ var MollifierBuffer = class {
|
|
|
16966
17005
|
// never wiped by a slow predecessor.
|
|
16967
17006
|
async releaseClaim(input) {
|
|
16968
17007
|
const claimKey = makeIdempotencyClaimKey(input);
|
|
16969
|
-
await this.redis.releaseMollifierClaim(
|
|
16970
|
-
claimKey,
|
|
16971
|
-
`${PENDING_PREFIX}${input.token}`
|
|
16972
|
-
);
|
|
17008
|
+
await this.redis.releaseMollifierClaim(claimKey, `${PENDING_PREFIX}${input.token}`);
|
|
16973
17009
|
}
|
|
16974
17010
|
// Read the current claim value, used by the wait/poll loop on losers
|
|
16975
17011
|
// to detect "pending" → "resolved" transitions and timeouts.
|
|
@@ -17654,9 +17690,7 @@ var MollifierDrainer = class {
|
|
|
17654
17690
|
const orgs = await this.buffer.listOrgs();
|
|
17655
17691
|
if (orgs.length === 0) return { drained: 0, failed: 0 };
|
|
17656
17692
|
const orgSlice = this.takeOrgSlice(orgs);
|
|
17657
|
-
const envsByOrg = await Promise.all(
|
|
17658
|
-
orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId))
|
|
17659
|
-
);
|
|
17693
|
+
const envsByOrg = await Promise.all(orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId)));
|
|
17660
17694
|
const targets = [];
|
|
17661
17695
|
for (let i = 0; i < orgSlice.length; i++) {
|
|
17662
17696
|
const orgId = orgSlice[i];
|
|
@@ -17869,14 +17903,11 @@ var MollifierDrainer = class {
|
|
|
17869
17903
|
} catch (writeErr) {
|
|
17870
17904
|
if (this.isRetryable(writeErr)) {
|
|
17871
17905
|
await this.buffer.requeue(entry.runId);
|
|
17872
|
-
this.logger.warn(
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
writeErr
|
|
17878
|
-
}
|
|
17879
|
-
);
|
|
17906
|
+
this.logger.warn("MollifierDrainer: terminal-failure callback retryable; requeued", {
|
|
17907
|
+
runId: entry.runId,
|
|
17908
|
+
attempts: nextAttempts,
|
|
17909
|
+
writeErr
|
|
17910
|
+
});
|
|
17880
17911
|
return "failed";
|
|
17881
17912
|
}
|
|
17882
17913
|
this.logger.error("MollifierDrainer: terminal-failure callback failed", {
|