@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.d.cts
CHANGED
|
@@ -64,6 +64,22 @@ declare class SimpleQueue<TMessageCatalog extends MessageCatalogSchema> {
|
|
|
64
64
|
size({ includeFuture }?: {
|
|
65
65
|
includeFuture?: boolean;
|
|
66
66
|
}): Promise<number>;
|
|
67
|
+
/**
|
|
68
|
+
* Age (in ms) of the oldest *overdue* message — the oldest item whose scheduled
|
|
69
|
+
* time has already passed (score <= now). Returns 0 when the queue is empty or
|
|
70
|
+
* only holds future/delayed or in-flight (future-scored) items.
|
|
71
|
+
*
|
|
72
|
+
* Resolves the candidate against the `items` hash so orphaned `queue` entries
|
|
73
|
+
* (a member whose payload is missing — the same stale state `dequeueItems`
|
|
74
|
+
* cleans up) don't report a phantom stall for work that can't be dequeued. The
|
|
75
|
+
* Lua scans due items oldest-first and returns the first score whose payload
|
|
76
|
+
* still exists.
|
|
77
|
+
*
|
|
78
|
+
* This is the generic stall signal: it stays at 0 while a queue drains healthily
|
|
79
|
+
* and rises only when due work sits undrained (poison block, dead consumer,
|
|
80
|
+
* backpressure).
|
|
81
|
+
*/
|
|
82
|
+
oldestMessageAge(): Promise<number>;
|
|
67
83
|
getJob(id: string): Promise<QueueItem<TMessageCatalog> | null>;
|
|
68
84
|
moveToDeadLetterQueue(id: string, errorMessage: string): Promise<void>;
|
|
69
85
|
sizeOfDeadLetterQueue(): Promise<number>;
|
|
@@ -80,6 +96,7 @@ declare module "@internal/redis" {
|
|
|
80
96
|
moveToDeadLetterQueue(queue: string, items: string, dlq: string, dlqItems: string, id: string, errorMessage: string, callback?: Callback<number>): Result<number, Context>;
|
|
81
97
|
enqueueItemOnce(queue: string, items: string, id: string, score: number, serializedItem: string, callback?: Callback<number>): Result<number, Context>;
|
|
82
98
|
getJob(queue: string, items: string, id: string, callback?: Callback<[string, string, string] | null>): Result<[string, string, string] | null, Context>;
|
|
99
|
+
getOldestDueScore(queue: string, items: string, now: number, callback?: Callback<string | number>): Result<string | number, Context>;
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,22 @@ declare class SimpleQueue<TMessageCatalog extends MessageCatalogSchema> {
|
|
|
64
64
|
size({ includeFuture }?: {
|
|
65
65
|
includeFuture?: boolean;
|
|
66
66
|
}): Promise<number>;
|
|
67
|
+
/**
|
|
68
|
+
* Age (in ms) of the oldest *overdue* message — the oldest item whose scheduled
|
|
69
|
+
* time has already passed (score <= now). Returns 0 when the queue is empty or
|
|
70
|
+
* only holds future/delayed or in-flight (future-scored) items.
|
|
71
|
+
*
|
|
72
|
+
* Resolves the candidate against the `items` hash so orphaned `queue` entries
|
|
73
|
+
* (a member whose payload is missing — the same stale state `dequeueItems`
|
|
74
|
+
* cleans up) don't report a phantom stall for work that can't be dequeued. The
|
|
75
|
+
* Lua scans due items oldest-first and returns the first score whose payload
|
|
76
|
+
* still exists.
|
|
77
|
+
*
|
|
78
|
+
* This is the generic stall signal: it stays at 0 while a queue drains healthily
|
|
79
|
+
* and rises only when due work sits undrained (poison block, dead consumer,
|
|
80
|
+
* backpressure).
|
|
81
|
+
*/
|
|
82
|
+
oldestMessageAge(): Promise<number>;
|
|
67
83
|
getJob(id: string): Promise<QueueItem<TMessageCatalog> | null>;
|
|
68
84
|
moveToDeadLetterQueue(id: string, errorMessage: string): Promise<void>;
|
|
69
85
|
sizeOfDeadLetterQueue(): Promise<number>;
|
|
@@ -80,6 +96,7 @@ declare module "@internal/redis" {
|
|
|
80
96
|
moveToDeadLetterQueue(queue: string, items: string, dlq: string, dlqItems: string, id: string, errorMessage: string, callback?: Callback<number>): Result<number, Context>;
|
|
81
97
|
enqueueItemOnce(queue: string, items: string, id: string, score: number, serializedItem: string, callback?: Callback<number>): Result<number, Context>;
|
|
82
98
|
getJob(queue: string, items: string, id: string, callback?: Callback<[string, string, string] | null>): Result<[string, string, string] | null, Context>;
|
|
99
|
+
getOldestDueScore(queue: string, items: string, now: number, callback?: Callback<string | number>): Result<string | number, Context>;
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
|
package/dist/index.js
CHANGED
|
@@ -9985,6 +9985,37 @@ var SimpleQueue = class {
|
|
|
9985
9985
|
throw e;
|
|
9986
9986
|
}
|
|
9987
9987
|
}
|
|
9988
|
+
/**
|
|
9989
|
+
* Age (in ms) of the oldest *overdue* message — the oldest item whose scheduled
|
|
9990
|
+
* time has already passed (score <= now). Returns 0 when the queue is empty or
|
|
9991
|
+
* only holds future/delayed or in-flight (future-scored) items.
|
|
9992
|
+
*
|
|
9993
|
+
* Resolves the candidate against the `items` hash so orphaned `queue` entries
|
|
9994
|
+
* (a member whose payload is missing — the same stale state `dequeueItems`
|
|
9995
|
+
* cleans up) don't report a phantom stall for work that can't be dequeued. The
|
|
9996
|
+
* Lua scans due items oldest-first and returns the first score whose payload
|
|
9997
|
+
* still exists.
|
|
9998
|
+
*
|
|
9999
|
+
* This is the generic stall signal: it stays at 0 while a queue drains healthily
|
|
10000
|
+
* and rises only when due work sits undrained (poison block, dead consumer,
|
|
10001
|
+
* backpressure).
|
|
10002
|
+
*/
|
|
10003
|
+
async oldestMessageAge() {
|
|
10004
|
+
try {
|
|
10005
|
+
const now = Date.now();
|
|
10006
|
+
const score = Number(await this.redis.getOldestDueScore(`queue`, `items`, now));
|
|
10007
|
+
if (!Number.isFinite(score) || score < 0) {
|
|
10008
|
+
return 0;
|
|
10009
|
+
}
|
|
10010
|
+
return Math.max(0, now - score);
|
|
10011
|
+
} catch (e) {
|
|
10012
|
+
this.logger.error(`SimpleQueue ${this.name}.oldestMessageAge(): error getting oldest age`, {
|
|
10013
|
+
queue: this.name,
|
|
10014
|
+
error: e
|
|
10015
|
+
});
|
|
10016
|
+
return 0;
|
|
10017
|
+
}
|
|
10018
|
+
}
|
|
9988
10019
|
async getJob(id) {
|
|
9989
10020
|
const result = await this.redis.getJob(`queue`, `items`, id);
|
|
9990
10021
|
if (!result) {
|
|
@@ -10151,6 +10182,29 @@ var SimpleQueue = class {
|
|
|
10151
10182
|
return dequeued
|
|
10152
10183
|
`
|
|
10153
10184
|
});
|
|
10185
|
+
this.redis.defineCommand("getOldestDueScore", {
|
|
10186
|
+
numberOfKeys: 2,
|
|
10187
|
+
lua: `
|
|
10188
|
+
local queue = KEYS[1]
|
|
10189
|
+
local items = KEYS[2]
|
|
10190
|
+
local now = tonumber(ARGV[1])
|
|
10191
|
+
|
|
10192
|
+
-- Oldest-first scan of due items, bounded so a long prefix of orphans can't
|
|
10193
|
+
-- make this O(n). Orphans are rare (dequeueItems removes them), so in the
|
|
10194
|
+
-- common case this returns on the first iteration. Read-only: unlike
|
|
10195
|
+
-- dequeueItems we don't ZREM orphans here \u2014 a metric probe must not mutate.
|
|
10196
|
+
local result = redis.call('ZRANGEBYSCORE', queue, '-inf', now, 'WITHSCORES', 'LIMIT', 0, 100)
|
|
10197
|
+
|
|
10198
|
+
for i = 1, #result, 2 do
|
|
10199
|
+
local id = result[i]
|
|
10200
|
+
if redis.call('HEXISTS', items, id) == 1 then
|
|
10201
|
+
return result[i + 1]
|
|
10202
|
+
end
|
|
10203
|
+
end
|
|
10204
|
+
|
|
10205
|
+
return -1
|
|
10206
|
+
`
|
|
10207
|
+
});
|
|
10154
10208
|
this.redis.defineCommand("getJob", {
|
|
10155
10209
|
numberOfKeys: 2,
|
|
10156
10210
|
lua: `
|
|
@@ -11395,6 +11449,15 @@ var Worker = class _Worker {
|
|
|
11395
11449
|
concurrencyLimitPendingObservableGauge.addCallback(
|
|
11396
11450
|
this.#updateConcurrencyLimitPendingMetric.bind(this)
|
|
11397
11451
|
);
|
|
11452
|
+
const oldestMessageAgeObservableGauge = this.meter.createObservableGauge(
|
|
11453
|
+
"redis_worker.queue.oldest_message_age",
|
|
11454
|
+
{
|
|
11455
|
+
description: "Age of the oldest overdue message in the queue",
|
|
11456
|
+
unit: "ms",
|
|
11457
|
+
valueType: ValueType.INT
|
|
11458
|
+
}
|
|
11459
|
+
);
|
|
11460
|
+
oldestMessageAgeObservableGauge.addCallback(this.#updateOldestMessageAgeMetric.bind(this));
|
|
11398
11461
|
}
|
|
11399
11462
|
subscriber;
|
|
11400
11463
|
tracer;
|
|
@@ -11423,6 +11486,12 @@ var Worker = class _Worker {
|
|
|
11423
11486
|
worker_name: this.options.name
|
|
11424
11487
|
});
|
|
11425
11488
|
}
|
|
11489
|
+
async #updateOldestMessageAgeMetric(observableResult) {
|
|
11490
|
+
const oldestMessageAge = await this.queue.oldestMessageAge();
|
|
11491
|
+
observableResult.observe(oldestMessageAge, {
|
|
11492
|
+
worker_name: this.options.name
|
|
11493
|
+
});
|
|
11494
|
+
}
|
|
11426
11495
|
async #updateConcurrencyLimitActiveMetric(observableResult) {
|
|
11427
11496
|
for (const [workerId, limiter] of Object.entries(this.limiters)) {
|
|
11428
11497
|
observableResult.observe(limiter.activeCount, {
|
|
@@ -11716,15 +11785,15 @@ var Worker = class _Worker {
|
|
|
11716
11785
|
await this.flushBatch(queueItem.job, workerId, limiter);
|
|
11717
11786
|
}
|
|
11718
11787
|
} else {
|
|
11719
|
-
limiter(
|
|
11720
|
-
() =>
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
}
|
|
11727
|
-
|
|
11788
|
+
limiter(() => this.processItem(queueItem, items.length, workerId, limiter)).catch(
|
|
11789
|
+
(err) => {
|
|
11790
|
+
this.logger.error("Unhandled error in processItem:", {
|
|
11791
|
+
error: err,
|
|
11792
|
+
workerId,
|
|
11793
|
+
item
|
|
11794
|
+
});
|
|
11795
|
+
}
|
|
11796
|
+
);
|
|
11728
11797
|
}
|
|
11729
11798
|
}
|
|
11730
11799
|
} catch (error) {
|
|
@@ -11874,13 +11943,25 @@ var Worker = class _Worker {
|
|
|
11874
11943
|
attempt: newAttempt
|
|
11875
11944
|
};
|
|
11876
11945
|
if (!shouldLogError) {
|
|
11877
|
-
this.logger.info(
|
|
11946
|
+
this.logger.info(
|
|
11947
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11948
|
+
dlqLogAttributes
|
|
11949
|
+
);
|
|
11878
11950
|
} else if (errorLogLevel === "warn") {
|
|
11879
|
-
this.logger.warn(
|
|
11951
|
+
this.logger.warn(
|
|
11952
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11953
|
+
dlqLogAttributes
|
|
11954
|
+
);
|
|
11880
11955
|
} else if (errorLogLevel === "info") {
|
|
11881
|
-
this.logger.info(
|
|
11956
|
+
this.logger.info(
|
|
11957
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11958
|
+
dlqLogAttributes
|
|
11959
|
+
);
|
|
11882
11960
|
} else {
|
|
11883
|
-
this.logger.error(
|
|
11961
|
+
this.logger.error(
|
|
11962
|
+
`Worker batch item reached max attempts. Moving to DLQ.`,
|
|
11963
|
+
dlqLogAttributes
|
|
11964
|
+
);
|
|
11884
11965
|
}
|
|
11885
11966
|
await this.queue.moveToDeadLetterQueue(item.id, errorMessage);
|
|
11886
11967
|
continue;
|
|
@@ -14389,9 +14470,7 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
14389
14470
|
};
|
|
14390
14471
|
})
|
|
14391
14472
|
);
|
|
14392
|
-
const eligibleTenants = tenantData.filter(
|
|
14393
|
-
(t) => !t.isAtCapacity && t.deficit >= 1
|
|
14394
|
-
);
|
|
14473
|
+
const eligibleTenants = tenantData.filter((t) => !t.isAtCapacity && t.deficit >= 1);
|
|
14395
14474
|
const blockedTenants = tenantData.filter((t) => t.isAtCapacity);
|
|
14396
14475
|
if (blockedTenants.length > 0) {
|
|
14397
14476
|
this.logger.debug("DRR: tenants blocked by concurrency", {
|
|
@@ -14684,11 +14763,7 @@ var WeightedScheduler = class extends BaseScheduler {
|
|
|
14684
14763
|
// FairScheduler Implementation
|
|
14685
14764
|
// ============================================================================
|
|
14686
14765
|
async selectQueues(masterQueueShard, consumerId, context2) {
|
|
14687
|
-
const snapshot = await this.#getOrCreateSnapshot(
|
|
14688
|
-
masterQueueShard,
|
|
14689
|
-
consumerId,
|
|
14690
|
-
context2
|
|
14691
|
-
);
|
|
14766
|
+
const snapshot = await this.#getOrCreateSnapshot(masterQueueShard, consumerId, context2);
|
|
14692
14767
|
if (snapshot.queues.length === 0) {
|
|
14693
14768
|
return [];
|
|
14694
14769
|
}
|
|
@@ -16061,10 +16136,7 @@ var FairQueue = class {
|
|
|
16061
16136
|
if (this.concurrencyManager && storedMessage) {
|
|
16062
16137
|
await this.concurrencyManager.release(descriptor, messageId);
|
|
16063
16138
|
}
|
|
16064
|
-
const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(
|
|
16065
|
-
queueId,
|
|
16066
|
-
descriptor.tenantId
|
|
16067
|
-
);
|
|
16139
|
+
const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId);
|
|
16068
16140
|
if (queueEmpty) {
|
|
16069
16141
|
this.queueDescriptorCache.delete(queueId);
|
|
16070
16142
|
this.queueCooloffStates.delete(queueId);
|
|
@@ -16321,38 +16393,6 @@ var FairQueue = class {
|
|
|
16321
16393
|
}
|
|
16322
16394
|
return false;
|
|
16323
16395
|
}
|
|
16324
|
-
#incrementCooloff(queueId) {
|
|
16325
|
-
if (this.queueCooloffStates.size >= this.maxCooloffStatesSize) {
|
|
16326
|
-
this.logger.warn("Cooloff states cache hit size cap, clearing all entries", {
|
|
16327
|
-
size: this.queueCooloffStates.size,
|
|
16328
|
-
cap: this.maxCooloffStatesSize
|
|
16329
|
-
});
|
|
16330
|
-
this.queueCooloffStates.clear();
|
|
16331
|
-
}
|
|
16332
|
-
const state = this.queueCooloffStates.get(queueId) ?? {
|
|
16333
|
-
tag: "normal",
|
|
16334
|
-
consecutiveFailures: 0
|
|
16335
|
-
};
|
|
16336
|
-
if (state.tag === "normal") {
|
|
16337
|
-
const newFailures = state.consecutiveFailures + 1;
|
|
16338
|
-
if (newFailures >= this.cooloffThreshold) {
|
|
16339
|
-
this.queueCooloffStates.set(queueId, {
|
|
16340
|
-
tag: "cooloff",
|
|
16341
|
-
expiresAt: Date.now() + this.cooloffPeriodMs
|
|
16342
|
-
});
|
|
16343
|
-
this.logger.debug("Queue entered cooloff", {
|
|
16344
|
-
queueId,
|
|
16345
|
-
cooloffPeriodMs: this.cooloffPeriodMs,
|
|
16346
|
-
consecutiveFailures: newFailures
|
|
16347
|
-
});
|
|
16348
|
-
} else {
|
|
16349
|
-
this.queueCooloffStates.set(queueId, {
|
|
16350
|
-
tag: "normal",
|
|
16351
|
-
consecutiveFailures: newFailures
|
|
16352
|
-
});
|
|
16353
|
-
}
|
|
16354
|
-
}
|
|
16355
|
-
}
|
|
16356
16396
|
#resetCooloff(queueId) {
|
|
16357
16397
|
this.queueCooloffStates.delete(queueId);
|
|
16358
16398
|
}
|
|
@@ -16620,7 +16660,10 @@ var stringToError = z.string().transform((v, ctx) => {
|
|
|
16620
16660
|
try {
|
|
16621
16661
|
return BufferEntryError.parse(JSON.parse(v));
|
|
16622
16662
|
} catch {
|
|
16623
|
-
ctx.addIssue({
|
|
16663
|
+
ctx.addIssue({
|
|
16664
|
+
code: z.ZodIssueCode.custom,
|
|
16665
|
+
message: "expected JSON-encoded BufferEntryError"
|
|
16666
|
+
});
|
|
16624
16667
|
return z.NEVER;
|
|
16625
16668
|
}
|
|
16626
16669
|
});
|
|
@@ -16818,11 +16861,7 @@ var MollifierBuffer = class {
|
|
|
16818
16861
|
// not an error.
|
|
16819
16862
|
async listEntriesForEnv(envId, maxCount) {
|
|
16820
16863
|
if (maxCount <= 0) return [];
|
|
16821
|
-
const runIds = await this.redis.lrange(
|
|
16822
|
-
`mollifier:queue:${envId}`,
|
|
16823
|
-
0,
|
|
16824
|
-
maxCount - 1
|
|
16825
|
-
);
|
|
16864
|
+
const runIds = await this.redis.lrange(`mollifier:queue:${envId}`, 0, maxCount - 1);
|
|
16826
16865
|
if (runIds.length === 0) return [];
|
|
16827
16866
|
const pipeline = this.redis.pipeline();
|
|
16828
16867
|
for (const runId of runIds) {
|
|
@@ -16959,10 +16998,7 @@ var MollifierBuffer = class {
|
|
|
16959
16998
|
// never wiped by a slow predecessor.
|
|
16960
16999
|
async releaseClaim(input) {
|
|
16961
17000
|
const claimKey = makeIdempotencyClaimKey(input);
|
|
16962
|
-
await this.redis.releaseMollifierClaim(
|
|
16963
|
-
claimKey,
|
|
16964
|
-
`${PENDING_PREFIX}${input.token}`
|
|
16965
|
-
);
|
|
17001
|
+
await this.redis.releaseMollifierClaim(claimKey, `${PENDING_PREFIX}${input.token}`);
|
|
16966
17002
|
}
|
|
16967
17003
|
// Read the current claim value, used by the wait/poll loop on losers
|
|
16968
17004
|
// to detect "pending" → "resolved" transitions and timeouts.
|
|
@@ -17647,9 +17683,7 @@ var MollifierDrainer = class {
|
|
|
17647
17683
|
const orgs = await this.buffer.listOrgs();
|
|
17648
17684
|
if (orgs.length === 0) return { drained: 0, failed: 0 };
|
|
17649
17685
|
const orgSlice = this.takeOrgSlice(orgs);
|
|
17650
|
-
const envsByOrg = await Promise.all(
|
|
17651
|
-
orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId))
|
|
17652
|
-
);
|
|
17686
|
+
const envsByOrg = await Promise.all(orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId)));
|
|
17653
17687
|
const targets = [];
|
|
17654
17688
|
for (let i = 0; i < orgSlice.length; i++) {
|
|
17655
17689
|
const orgId = orgSlice[i];
|
|
@@ -17862,14 +17896,11 @@ var MollifierDrainer = class {
|
|
|
17862
17896
|
} catch (writeErr) {
|
|
17863
17897
|
if (this.isRetryable(writeErr)) {
|
|
17864
17898
|
await this.buffer.requeue(entry.runId);
|
|
17865
|
-
this.logger.warn(
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
writeErr
|
|
17871
|
-
}
|
|
17872
|
-
);
|
|
17899
|
+
this.logger.warn("MollifierDrainer: terminal-failure callback retryable; requeued", {
|
|
17900
|
+
runId: entry.runId,
|
|
17901
|
+
attempts: nextAttempts,
|
|
17902
|
+
writeErr
|
|
17903
|
+
});
|
|
17873
17904
|
return "failed";
|
|
17874
17905
|
}
|
|
17875
17906
|
this.logger.error("MollifierDrainer: terminal-failure callback failed", {
|