@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 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
- () => this.processItem(queueItem, items.length, workerId, limiter)
11728
- ).catch((err) => {
11729
- this.logger.error("Unhandled error in processItem:", {
11730
- error: err,
11731
- workerId,
11732
- item
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(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
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(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
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(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
11963
+ this.logger.info(
11964
+ `Worker batch item reached max attempts. Moving to DLQ.`,
11965
+ dlqLogAttributes
11966
+ );
11889
11967
  } else {
11890
- this.logger.error(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
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({ code: zod.z.ZodIssueCode.custom, message: "expected JSON-encoded BufferEntryError" });
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
- "MollifierDrainer: terminal-failure callback retryable; requeued",
17874
- {
17875
- runId: entry.runId,
17876
- attempts: nextAttempts,
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", {