@trigger.dev/redis-worker 0.0.0-prerelease-20251209163704 → 0.0.0-prerelease-20251215135620

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
@@ -11784,12 +11784,7 @@ var ConcurrencyManager = class {
11784
11784
  );
11785
11785
  const keys = groupData.map((g) => g.key);
11786
11786
  const limits = groupData.map((g) => g.limit.toString());
11787
- const result = await this.redis.reserveConcurrency(
11788
- keys.length.toString(),
11789
- messageId,
11790
- ...keys,
11791
- ...limits
11792
- );
11787
+ const result = await this.redis.reserveConcurrency(keys.length, keys, messageId, ...limits);
11793
11788
  return result === 1;
11794
11789
  }
11795
11790
  /**
@@ -11885,16 +11880,14 @@ var ConcurrencyManager = class {
11885
11880
  // ============================================================================
11886
11881
  #registerCommands() {
11887
11882
  this.redis.defineCommand("reserveConcurrency", {
11888
- numberOfKeys: 0,
11889
- // Will pass number of keys in ARGV
11890
11883
  lua: `
11891
- local numGroups = tonumber(ARGV[1])
11892
- local messageId = ARGV[2]
11884
+ local numGroups = #KEYS
11885
+ local messageId = ARGV[1]
11893
11886
 
11894
11887
  -- Check all groups first
11895
11888
  for i = 1, numGroups do
11896
- local key = ARGV[2 + i] -- Keys start at ARGV[3]
11897
- local limit = tonumber(ARGV[2 + numGroups + i]) -- Limits come after keys
11889
+ local key = KEYS[i]
11890
+ local limit = tonumber(ARGV[1 + i]) -- Limits start at ARGV[2]
11898
11891
  local current = redis.call('SCARD', key)
11899
11892
 
11900
11893
  if current >= limit then
@@ -11904,7 +11897,7 @@ end
11904
11897
 
11905
11898
  -- All groups have capacity, add message to all
11906
11899
  for i = 1, numGroups do
11907
- local key = ARGV[2 + i]
11900
+ local key = KEYS[i]
11908
11901
  redis.call('SADD', key, messageId)
11909
11902
  end
11910
11903
 
@@ -12517,8 +12510,12 @@ var VisibilityManager = class {
12517
12510
  const inflightKey = this.keys.inflightKey(shardId);
12518
12511
  const member = this.#makeMember(messageId, queueId);
12519
12512
  const newDeadline = Date.now() + extendMs;
12520
- const result = await this.redis.zadd(inflightKey, "XX", newDeadline, member);
12521
- const success = result !== 0;
12513
+ const result = await this.redis.heartbeatMessage(
12514
+ inflightKey,
12515
+ member,
12516
+ newDeadline.toString()
12517
+ );
12518
+ const success = result === 1;
12522
12519
  if (success) {
12523
12520
  this.logger.debug("Heartbeat successful", {
12524
12521
  messageId,
@@ -12775,6 +12772,24 @@ redis.call('HDEL', inflightDataKey, messageId)
12775
12772
  redis.call('ZADD', queueKey, score, messageId)
12776
12773
  redis.call('HSET', queueItemsKey, messageId, payload)
12777
12774
 
12775
+ return 1
12776
+ `
12777
+ });
12778
+ this.redis.defineCommand("heartbeatMessage", {
12779
+ numberOfKeys: 1,
12780
+ lua: `
12781
+ local inflightKey = KEYS[1]
12782
+ local member = ARGV[1]
12783
+ local newDeadline = tonumber(ARGV[2])
12784
+
12785
+ -- Check if member exists in the in-flight set
12786
+ local score = redis.call('ZSCORE', inflightKey, member)
12787
+ if not score then
12788
+ return 0
12789
+ end
12790
+
12791
+ -- Update the deadline
12792
+ redis.call('ZADD', inflightKey, 'XX', newDeadline, member)
12778
12793
  return 1
12779
12794
  `
12780
12795
  });
@@ -13222,7 +13237,20 @@ var DRRScheduler = class extends BaseScheduler {
13222
13237
  const eligibleTenants = tenantData.filter(
13223
13238
  (t) => !t.isAtCapacity && t.deficit >= 1
13224
13239
  );
13240
+ const blockedTenants = tenantData.filter((t) => t.isAtCapacity);
13241
+ if (blockedTenants.length > 0) {
13242
+ this.logger.debug("DRR: tenants blocked by concurrency", {
13243
+ blockedCount: blockedTenants.length,
13244
+ blockedTenants: blockedTenants.map((t) => t.tenantId)
13245
+ });
13246
+ }
13225
13247
  eligibleTenants.sort((a, b) => b.deficit - a.deficit);
13248
+ this.logger.debug("DRR: queue selection complete", {
13249
+ totalQueues: queues.length,
13250
+ totalTenants: tenantIds.length,
13251
+ eligibleTenants: eligibleTenants.length,
13252
+ topTenantDeficit: eligibleTenants[0]?.deficit
13253
+ });
13226
13254
  return eligibleTenants.map((t) => ({
13227
13255
  tenantId: t.tenantId,
13228
13256
  queues: t.queues
@@ -13527,7 +13555,10 @@ var WeightedScheduler = class extends BaseScheduler {
13527
13555
  return { tenantId, avgAge };
13528
13556
  });
13529
13557
  const maxAge = Math.max(...tenantAges.map((t) => t.avgAge));
13530
- const weightedTenants = tenantAges.map((t) => ({
13558
+ const weightedTenants = maxAge === 0 ? tenantAges.map((t) => ({
13559
+ tenantId: t.tenantId,
13560
+ weight: 1 / tenantAges.length
13561
+ })) : tenantAges.map((t) => ({
13531
13562
  tenantId: t.tenantId,
13532
13563
  weight: t.avgAge / maxAge
13533
13564
  }));
@@ -13570,11 +13601,11 @@ var WeightedScheduler = class extends BaseScheduler {
13570
13601
  const tenant = snapshot.tenants.get(tenantId);
13571
13602
  let weight = 1;
13572
13603
  if (concurrencyLimitBias > 0) {
13573
- const normalizedLimit = tenant.concurrency.limit / maxLimit;
13604
+ const normalizedLimit = maxLimit > 0 ? tenant.concurrency.limit / maxLimit : 0;
13574
13605
  weight *= 1 + Math.pow(normalizedLimit * concurrencyLimitBias, 2);
13575
13606
  }
13576
13607
  if (availableCapacityBias > 0) {
13577
- const usedPercentage = tenant.concurrency.current / tenant.concurrency.limit;
13608
+ const usedPercentage = tenant.concurrency.limit > 0 ? tenant.concurrency.current / tenant.concurrency.limit : 1;
13578
13609
  const availableBonus = 1 - usedPercentage;
13579
13610
  weight *= 1 + Math.pow(availableBonus * availableCapacityBias, 2);
13580
13611
  }
@@ -13593,9 +13624,10 @@ var WeightedScheduler = class extends BaseScheduler {
13593
13624
  return queues.sort((a, b) => b.age - a.age).map((q) => q.queueId);
13594
13625
  }
13595
13626
  const maxAge = Math.max(...queues.map((q) => q.age));
13627
+ const ageDenom = maxAge === 0 ? 1 : maxAge;
13596
13628
  const weightedQueues = queues.map((q) => ({
13597
13629
  queue: q,
13598
- weight: 1 + q.age / maxAge * queueAgeRandomization
13630
+ weight: 1 + q.age / ageDenom * queueAgeRandomization
13599
13631
  }));
13600
13632
  const result = [];
13601
13633
  let remaining = [...weightedQueues];
@@ -13878,6 +13910,7 @@ var FairQueue = class {
13878
13910
  this.cooloffEnabled = options.cooloff?.enabled ?? true;
13879
13911
  this.cooloffThreshold = options.cooloff?.threshold ?? 10;
13880
13912
  this.cooloffPeriodMs = options.cooloff?.periodMs ?? 1e4;
13913
+ this.globalRateLimiter = options.globalRateLimiter;
13881
13914
  this.telemetry = new FairQueueTelemetry({
13882
13915
  tracer: options.tracer,
13883
13916
  meter: options.meter,
@@ -13948,6 +13981,8 @@ var FairQueue = class {
13948
13981
  cooloffThreshold;
13949
13982
  cooloffPeriodMs;
13950
13983
  queueCooloffStates = /* @__PURE__ */ new Map();
13984
+ // Global rate limiter
13985
+ globalRateLimiter;
13951
13986
  // Runtime state
13952
13987
  messageHandler;
13953
13988
  isRunning = false;
@@ -13958,6 +13993,30 @@ var FairQueue = class {
13958
13993
  // Queue descriptor cache for message processing
13959
13994
  queueDescriptorCache = /* @__PURE__ */ new Map();
13960
13995
  // ============================================================================
13996
+ // Public API - Telemetry
13997
+ // ============================================================================
13998
+ /**
13999
+ * Register observable gauge callbacks for telemetry.
14000
+ * Call this after FairQueue is created to enable gauge metrics.
14001
+ *
14002
+ * @param options.observedTenants - List of tenant IDs to observe for DLQ metrics
14003
+ */
14004
+ registerTelemetryGauges(options) {
14005
+ this.telemetry.registerGaugeCallbacks({
14006
+ getMasterQueueLength: async (shardId) => {
14007
+ return await this.masterQueue.getShardQueueCount(shardId);
14008
+ },
14009
+ getInflightCount: async (shardId) => {
14010
+ return await this.visibilityManager.getInflightCount(shardId);
14011
+ },
14012
+ getDLQLength: async (tenantId) => {
14013
+ return await this.getDeadLetterQueueLength(tenantId);
14014
+ },
14015
+ shardCount: this.shardCount,
14016
+ observedTenants: options?.observedTenants
14017
+ });
14018
+ }
14019
+ // ============================================================================
13961
14020
  // Public API - Message Handler
13962
14021
  // ============================================================================
13963
14022
  /**
@@ -14409,6 +14468,16 @@ var FairQueue = class {
14409
14468
  return false;
14410
14469
  }
14411
14470
  }
14471
+ if (this.globalRateLimiter) {
14472
+ const result = await this.globalRateLimiter.limit();
14473
+ if (!result.allowed && result.resetAt) {
14474
+ const waitMs = Math.max(0, result.resetAt - Date.now());
14475
+ if (waitMs > 0) {
14476
+ this.logger.debug("Global rate limit reached, waiting", { waitMs, loopId });
14477
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
14478
+ }
14479
+ }
14480
+ }
14412
14481
  const claimResult = await this.visibilityManager.claim(
14413
14482
  queueId,
14414
14483
  queueKey,
@@ -14534,18 +14603,33 @@ var FairQueue = class {
14534
14603
  return;
14535
14604
  }
14536
14605
  for (const { tenantId, queues } of tenantQueues) {
14537
- for (const queueId of queues) {
14538
- if (this.cooloffEnabled && this.#isInCooloff(queueId)) {
14539
- continue;
14606
+ let availableSlots = 1;
14607
+ if (this.concurrencyManager) {
14608
+ const [current, limit] = await Promise.all([
14609
+ this.concurrencyManager.getCurrentConcurrency("tenant", tenantId),
14610
+ this.concurrencyManager.getConcurrencyLimit("tenant", tenantId)
14611
+ ]);
14612
+ availableSlots = Math.max(1, limit - current);
14613
+ }
14614
+ let slotsUsed = 0;
14615
+ queueLoop: for (const queueId of queues) {
14616
+ while (slotsUsed < availableSlots) {
14617
+ if (this.cooloffEnabled && this.#isInCooloff(queueId)) {
14618
+ break;
14619
+ }
14620
+ const processed = await this.#processOneMessage(loopId, queueId, tenantId, shardId);
14621
+ if (processed) {
14622
+ await this.scheduler.recordProcessed?.(tenantId, queueId);
14623
+ this.#resetCooloff(queueId);
14624
+ slotsUsed++;
14625
+ } else {
14626
+ this.#incrementCooloff(queueId);
14627
+ break;
14628
+ }
14540
14629
  }
14541
- const processed = await this.#processOneMessage(loopId, queueId, tenantId, shardId);
14542
- if (processed) {
14543
- await this.scheduler.recordProcessed?.(tenantId, queueId);
14544
- this.#resetCooloff(queueId);
14545
- } else {
14546
- this.#incrementCooloff(queueId);
14630
+ if (slotsUsed >= availableSlots) {
14631
+ break queueLoop;
14547
14632
  }
14548
- break;
14549
14633
  }
14550
14634
  }
14551
14635
  }
@@ -14564,6 +14648,16 @@ var FairQueue = class {
14564
14648
  return false;
14565
14649
  }
14566
14650
  }
14651
+ if (this.globalRateLimiter) {
14652
+ const result = await this.globalRateLimiter.limit();
14653
+ if (!result.allowed && result.resetAt) {
14654
+ const waitMs = Math.max(0, result.resetAt - Date.now());
14655
+ if (waitMs > 0) {
14656
+ this.logger.debug("Global rate limit reached, waiting", { waitMs, loopId });
14657
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
14658
+ }
14659
+ }
14660
+ }
14567
14661
  const claimResult = await this.visibilityManager.claim(
14568
14662
  queueId,
14569
14663
  queueKey,
@@ -14610,6 +14704,17 @@ var FairQueue = class {
14610
14704
  error: result.error.message
14611
14705
  });
14612
14706
  await this.#moveToDeadLetterQueue(storedMessage, "Payload validation failed");
14707
+ if (this.concurrencyManager) {
14708
+ try {
14709
+ await this.concurrencyManager.release(descriptor, storedMessage.id);
14710
+ } catch (releaseError) {
14711
+ this.logger.error("Failed to release concurrency slot after payload validation failure", {
14712
+ messageId: storedMessage.id,
14713
+ queueId,
14714
+ error: releaseError instanceof Error ? releaseError.message : String(releaseError)
14715
+ });
14716
+ }
14717
+ }
14613
14718
  return;
14614
14719
  }
14615
14720
  payload = result.data;
@@ -14786,7 +14891,6 @@ var FairQueue = class {
14786
14891
  }
14787
14892
  async #moveToDeadLetterQueue(storedMessage, errorMessage) {
14788
14893
  if (!this.deadLetterQueueEnabled) {
14789
- this.masterQueue.getShardForQueue(storedMessage.queueId);
14790
14894
  await this.visibilityManager.complete(storedMessage.id, storedMessage.queueId);
14791
14895
  return;
14792
14896
  }
@@ -14888,6 +14992,11 @@ var FairQueue = class {
14888
14992
  tag: "cooloff",
14889
14993
  expiresAt: Date.now() + this.cooloffPeriodMs
14890
14994
  });
14995
+ this.logger.debug("Queue entered cooloff", {
14996
+ queueId,
14997
+ cooloffPeriodMs: this.cooloffPeriodMs,
14998
+ consecutiveFailures: newFailures
14999
+ });
14891
15000
  } else {
14892
15001
  this.queueCooloffStates.set(queueId, {
14893
15002
  tag: "normal",