@trigger.dev/redis-worker 4.4.0 → 4.4.2

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.js CHANGED
@@ -11161,6 +11161,8 @@ var Worker = class _Worker {
11161
11161
  shutdownTimeoutMs;
11162
11162
  // The p-limit limiter to control overall concurrency.
11163
11163
  limiters = {};
11164
+ // Batch accumulators: one per batch-enabled job type
11165
+ batchAccumulators = /* @__PURE__ */ new Map();
11164
11166
  async #updateQueueSizeMetric(observableResult) {
11165
11167
  const queueSize = await this.queue.size();
11166
11168
  observableResult.observe(queueSize, {
@@ -11363,6 +11365,31 @@ var Worker = class _Worker {
11363
11365
  async getJob(id) {
11364
11366
  return this.queue.getJob(id);
11365
11367
  }
11368
+ /**
11369
+ * Returns true if the given job type has batch config in the catalog.
11370
+ */
11371
+ isBatchJob(jobType) {
11372
+ const catalogItem = this.options.catalog[jobType];
11373
+ return !!catalogItem?.batch;
11374
+ }
11375
+ /**
11376
+ * Returns the batch config for a job type, or undefined if not batch-enabled.
11377
+ */
11378
+ getBatchConfig(jobType) {
11379
+ const catalogItem = this.options.catalog[jobType];
11380
+ return catalogItem?.batch;
11381
+ }
11382
+ /**
11383
+ * The max dequeue count: the largest batch maxSize across catalog entries,
11384
+ * falling back to tasksPerWorker for non-batch catalogs.
11385
+ */
11386
+ get maxDequeueCount() {
11387
+ const batchSizes = Object.values(this.options.catalog).filter((entry) => !!entry.batch).map((entry) => entry.batch.maxSize);
11388
+ if (batchSizes.length > 0) {
11389
+ return Math.max(...batchSizes);
11390
+ }
11391
+ return this.concurrency.tasksPerWorker;
11392
+ }
11366
11393
  /**
11367
11394
  * The main loop that each worker runs. It repeatedly polls for items,
11368
11395
  * processes them, and then waits before the next iteration.
@@ -11386,6 +11413,7 @@ var Worker = class _Worker {
11386
11413
  concurrencyOptions: this.concurrency
11387
11414
  });
11388
11415
  while (!this.isShuttingDown) {
11416
+ await this.flushTimedOutBatches(workerId, limiter);
11389
11417
  if (limiter.activeCount + limiter.pendingCount >= this.concurrency.limit) {
11390
11418
  this.logger.debug("Worker at capacity, waiting", {
11391
11419
  workerId,
@@ -11397,9 +11425,10 @@ var Worker = class _Worker {
11397
11425
  continue;
11398
11426
  }
11399
11427
  const $taskCount = Math.min(
11400
- taskCount,
11428
+ this.maxDequeueCount,
11401
11429
  this.concurrency.limit - limiter.activeCount - limiter.pendingCount
11402
11430
  );
11431
+ let itemsFound = false;
11403
11432
  try {
11404
11433
  const items = await this.withHistogram(
11405
11434
  this.metrics.dequeueDuration,
@@ -11409,7 +11438,8 @@ var Worker = class _Worker {
11409
11438
  task_count: $taskCount
11410
11439
  }
11411
11440
  );
11412
- if (items.length === 0) {
11441
+ itemsFound = items.length > 0;
11442
+ if (items.length === 0 && this.batchAccumulators.size === 0) {
11413
11443
  this.logger.debug("No items to dequeue", {
11414
11444
  workerId,
11415
11445
  concurrencyOptions: this.concurrency,
@@ -11419,29 +11449,226 @@ var Worker = class _Worker {
11419
11449
  await _Worker.delay(pollIntervalMs);
11420
11450
  continue;
11421
11451
  }
11422
- this.logger.debug("Dequeued items", {
11423
- workerId,
11424
- itemCount: items.length,
11425
- concurrencyOptions: this.concurrency,
11426
- activeCount: limiter.activeCount,
11427
- pendingCount: limiter.pendingCount
11428
- });
11429
- for (const item of items) {
11430
- limiter(
11431
- () => this.processItem(item, items.length, workerId, limiter)
11432
- ).catch((err) => {
11433
- this.logger.error("Unhandled error in processItem:", { error: err, workerId, item });
11452
+ if (items.length > 0) {
11453
+ this.logger.debug("Dequeued items", {
11454
+ workerId,
11455
+ itemCount: items.length,
11456
+ concurrencyOptions: this.concurrency,
11457
+ activeCount: limiter.activeCount,
11458
+ pendingCount: limiter.pendingCount
11434
11459
  });
11435
11460
  }
11461
+ for (const item of items) {
11462
+ const queueItem = item;
11463
+ if (this.isBatchJob(queueItem.job)) {
11464
+ this.addToAccumulator(queueItem);
11465
+ const batchConfig = this.getBatchConfig(queueItem.job);
11466
+ const accumulator = this.batchAccumulators.get(queueItem.job);
11467
+ if (accumulator && accumulator.items.length >= batchConfig.maxSize) {
11468
+ await this.flushBatch(queueItem.job, workerId, limiter);
11469
+ }
11470
+ } else {
11471
+ limiter(
11472
+ () => this.processItem(queueItem, items.length, workerId, limiter)
11473
+ ).catch((err) => {
11474
+ this.logger.error("Unhandled error in processItem:", {
11475
+ error: err,
11476
+ workerId,
11477
+ item
11478
+ });
11479
+ });
11480
+ }
11481
+ }
11436
11482
  } catch (error) {
11437
11483
  this.logger.error("Error dequeuing items:", { name: this.options.name, error });
11438
11484
  await _Worker.delay(pollIntervalMs);
11439
11485
  continue;
11440
11486
  }
11441
- await _Worker.delay(immediatePollIntervalMs);
11487
+ if (itemsFound || this.batchAccumulators.size > 0) {
11488
+ await _Worker.delay(immediatePollIntervalMs);
11489
+ } else {
11490
+ await _Worker.delay(pollIntervalMs);
11491
+ }
11442
11492
  }
11493
+ await this.flushAllBatches(workerId, limiter);
11443
11494
  this.logger.info("Worker loop finished", { workerId });
11444
11495
  }
11496
+ /**
11497
+ * Adds an item to the batch accumulator for its job type.
11498
+ */
11499
+ addToAccumulator(item) {
11500
+ let accumulator = this.batchAccumulators.get(item.job);
11501
+ if (!accumulator) {
11502
+ accumulator = { items: [], firstItemAt: Date.now() };
11503
+ this.batchAccumulators.set(item.job, accumulator);
11504
+ }
11505
+ accumulator.items.push(item);
11506
+ }
11507
+ /**
11508
+ * Flushes any batch accumulators that have exceeded their maxWaitMs.
11509
+ */
11510
+ async flushTimedOutBatches(workerId, limiter) {
11511
+ const now = Date.now();
11512
+ for (const [jobType, accumulator] of this.batchAccumulators) {
11513
+ const batchConfig = this.getBatchConfig(jobType);
11514
+ if (!batchConfig) continue;
11515
+ if (now - accumulator.firstItemAt >= batchConfig.maxWaitMs) {
11516
+ await this.flushBatch(jobType, workerId, limiter);
11517
+ }
11518
+ }
11519
+ }
11520
+ /**
11521
+ * Flushes all batch accumulators (used during shutdown).
11522
+ */
11523
+ async flushAllBatches(workerId, limiter) {
11524
+ for (const jobType of this.batchAccumulators.keys()) {
11525
+ await this.flushBatch(jobType, workerId, limiter);
11526
+ }
11527
+ }
11528
+ /**
11529
+ * Flushes the batch accumulator for a specific job type.
11530
+ * Removes items from the accumulator and submits them to the limiter as a single batch.
11531
+ */
11532
+ async flushBatch(jobType, workerId, limiter) {
11533
+ const accumulator = this.batchAccumulators.get(jobType);
11534
+ if (!accumulator || accumulator.items.length === 0) return;
11535
+ const items = accumulator.items;
11536
+ this.batchAccumulators.delete(jobType);
11537
+ this.logger.debug("Flushing batch", {
11538
+ jobType,
11539
+ batchSize: items.length,
11540
+ workerId,
11541
+ accumulatedMs: Date.now() - accumulator.firstItemAt
11542
+ });
11543
+ limiter(() => this.processBatch(items, jobType, workerId, limiter)).catch((err) => {
11544
+ this.logger.error("Unhandled error in processBatch:", {
11545
+ error: err,
11546
+ workerId,
11547
+ jobType,
11548
+ batchSize: items.length
11549
+ });
11550
+ });
11551
+ }
11552
+ /**
11553
+ * Processes a batch of items for a batch-enabled job type.
11554
+ */
11555
+ async processBatch(items, jobType, workerId, limiter) {
11556
+ const catalogItem = this.options.catalog[jobType];
11557
+ const handler = this.jobs[jobType];
11558
+ if (!handler) {
11559
+ this.logger.error(`Worker no handler found for batch job type: ${jobType}`);
11560
+ return;
11561
+ }
11562
+ if (!catalogItem) {
11563
+ this.logger.error(`Worker no catalog item found for batch job type: ${jobType}`);
11564
+ return;
11565
+ }
11566
+ const batchParams = items.map((item) => ({
11567
+ id: item.id,
11568
+ payload: item.item,
11569
+ visibilityTimeoutMs: item.visibilityTimeoutMs,
11570
+ attempt: item.attempt,
11571
+ deduplicationKey: item.deduplicationKey
11572
+ }));
11573
+ await startSpan(
11574
+ this.tracer,
11575
+ "processBatch",
11576
+ async () => {
11577
+ await this.withHistogram(this.metrics.jobDuration, handler(batchParams), {
11578
+ worker_id: workerId,
11579
+ batch_size: items.length,
11580
+ job_type: jobType
11581
+ });
11582
+ await Promise.all(items.map((item) => this.queue.ack(item.id, item.deduplicationKey)));
11583
+ },
11584
+ {
11585
+ kind: SpanKind.CONSUMER,
11586
+ attributes: {
11587
+ job_type: jobType,
11588
+ batch_size: items.length,
11589
+ worker_id: workerId,
11590
+ worker_name: this.options.name
11591
+ }
11592
+ }
11593
+ ).catch(async (error) => {
11594
+ const errorMessage = error instanceof Error ? error.message : String(error);
11595
+ const shouldLogError = catalogItem.logErrors ?? true;
11596
+ if (shouldLogError) {
11597
+ this.logger.error(`Worker error processing batch`, {
11598
+ name: this.options.name,
11599
+ jobType,
11600
+ batchSize: items.length,
11601
+ error,
11602
+ errorMessage
11603
+ });
11604
+ } else {
11605
+ this.logger.info(`Worker failed to process batch`, {
11606
+ name: this.options.name,
11607
+ jobType,
11608
+ batchSize: items.length,
11609
+ error,
11610
+ errorMessage
11611
+ });
11612
+ }
11613
+ for (const item of items) {
11614
+ try {
11615
+ const newAttempt = item.attempt + 1;
11616
+ const retrySettings = {
11617
+ ...defaultRetrySettings,
11618
+ ...catalogItem?.retry
11619
+ };
11620
+ const retryDelay = calculateNextRetryDelay(retrySettings, newAttempt);
11621
+ if (!retryDelay) {
11622
+ if (shouldLogError) {
11623
+ this.logger.error(`Worker batch item reached max attempts. Moving to DLQ.`, {
11624
+ name: this.options.name,
11625
+ id: item.id,
11626
+ jobType,
11627
+ attempt: newAttempt
11628
+ });
11629
+ } else {
11630
+ this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, {
11631
+ name: this.options.name,
11632
+ id: item.id,
11633
+ jobType,
11634
+ attempt: newAttempt
11635
+ });
11636
+ }
11637
+ await this.queue.moveToDeadLetterQueue(item.id, errorMessage);
11638
+ continue;
11639
+ }
11640
+ const retryDate = new Date(Date.now() + retryDelay);
11641
+ this.logger.info(`Worker requeuing failed batch item with delay`, {
11642
+ name: this.options.name,
11643
+ id: item.id,
11644
+ jobType,
11645
+ retryDate,
11646
+ retryDelay,
11647
+ attempt: newAttempt
11648
+ });
11649
+ await this.queue.enqueue({
11650
+ id: item.id,
11651
+ job: item.job,
11652
+ item: item.item,
11653
+ availableAt: retryDate,
11654
+ attempt: newAttempt,
11655
+ visibilityTimeoutMs: item.visibilityTimeoutMs
11656
+ });
11657
+ } catch (requeueError) {
11658
+ this.logger.error(
11659
+ `Worker failed to requeue batch item. It will be retried after the visibility timeout.`,
11660
+ {
11661
+ name: this.options.name,
11662
+ id: item.id,
11663
+ jobType,
11664
+ visibilityTimeoutMs: item.visibilityTimeoutMs,
11665
+ error: requeueError
11666
+ }
11667
+ );
11668
+ }
11669
+ }
11670
+ });
11671
+ }
11445
11672
  /**
11446
11673
  * Processes a single item.
11447
11674
  */
@@ -12152,6 +12379,135 @@ return 1
12152
12379
  });
12153
12380
  }
12154
12381
  };
12382
+ var TenantDispatch = class {
12383
+ constructor(options) {
12384
+ this.options = options;
12385
+ this.redis = createRedisClient(options.redis);
12386
+ this.keys = options.keys;
12387
+ this.shardCount = Math.max(1, options.shardCount);
12388
+ }
12389
+ redis;
12390
+ keys;
12391
+ shardCount;
12392
+ /**
12393
+ * Get the dispatch shard ID for a tenant.
12394
+ * Uses jump consistent hash on the tenant ID so each tenant
12395
+ * always maps to exactly one dispatch shard.
12396
+ */
12397
+ getShardForTenant(tenantId) {
12398
+ return jumpHash(tenantId, this.shardCount);
12399
+ }
12400
+ /**
12401
+ * Get eligible tenants from a dispatch shard (Level 1).
12402
+ * Returns tenants ordered by oldest message (lowest score first).
12403
+ */
12404
+ async getTenantsFromShard(shardId, limit = 1e3, maxScore) {
12405
+ const dispatchKey = this.keys.dispatchKey(shardId);
12406
+ const score = maxScore ?? Date.now();
12407
+ const results = await this.redis.zrangebyscore(
12408
+ dispatchKey,
12409
+ "-inf",
12410
+ score,
12411
+ "WITHSCORES",
12412
+ "LIMIT",
12413
+ 0,
12414
+ limit
12415
+ );
12416
+ const tenants = [];
12417
+ for (let i = 0; i < results.length; i += 2) {
12418
+ const tenantId = results[i];
12419
+ const scoreStr = results[i + 1];
12420
+ if (tenantId && scoreStr) {
12421
+ tenants.push({
12422
+ tenantId,
12423
+ score: parseFloat(scoreStr)
12424
+ });
12425
+ }
12426
+ }
12427
+ return tenants;
12428
+ }
12429
+ /**
12430
+ * Get queues for a specific tenant (Level 2).
12431
+ * Returns queues ordered by oldest message (lowest score first).
12432
+ */
12433
+ async getQueuesForTenant(tenantId, limit = 1e3, maxScore) {
12434
+ const tenantQueueKey = this.keys.tenantQueueIndexKey(tenantId);
12435
+ const score = maxScore ?? Date.now();
12436
+ const results = await this.redis.zrangebyscore(
12437
+ tenantQueueKey,
12438
+ "-inf",
12439
+ score,
12440
+ "WITHSCORES",
12441
+ "LIMIT",
12442
+ 0,
12443
+ limit
12444
+ );
12445
+ const queues = [];
12446
+ for (let i = 0; i < results.length; i += 2) {
12447
+ const queueId = results[i];
12448
+ const scoreStr = results[i + 1];
12449
+ if (queueId && scoreStr) {
12450
+ queues.push({
12451
+ queueId,
12452
+ score: parseFloat(scoreStr),
12453
+ tenantId
12454
+ });
12455
+ }
12456
+ }
12457
+ return queues;
12458
+ }
12459
+ /**
12460
+ * Get the number of tenants in a dispatch shard.
12461
+ */
12462
+ async getShardTenantCount(shardId) {
12463
+ const dispatchKey = this.keys.dispatchKey(shardId);
12464
+ return await this.redis.zcard(dispatchKey);
12465
+ }
12466
+ /**
12467
+ * Get total tenant count across all dispatch shards.
12468
+ * Note: tenants may appear in multiple shards, so this may overcount.
12469
+ */
12470
+ async getTotalTenantCount() {
12471
+ const counts = await Promise.all(
12472
+ Array.from({ length: this.shardCount }, (_, i) => this.getShardTenantCount(i))
12473
+ );
12474
+ return counts.reduce((sum, count) => sum + count, 0);
12475
+ }
12476
+ /**
12477
+ * Get the number of queues for a tenant.
12478
+ */
12479
+ async getTenantQueueCount(tenantId) {
12480
+ const tenantQueueKey = this.keys.tenantQueueIndexKey(tenantId);
12481
+ return await this.redis.zcard(tenantQueueKey);
12482
+ }
12483
+ /**
12484
+ * Remove a tenant from a specific dispatch shard.
12485
+ */
12486
+ async removeTenantFromShard(shardId, tenantId) {
12487
+ const dispatchKey = this.keys.dispatchKey(shardId);
12488
+ await this.redis.zrem(dispatchKey, tenantId);
12489
+ }
12490
+ /**
12491
+ * Add a tenant to a dispatch shard with the given score.
12492
+ */
12493
+ async addTenantToShard(shardId, tenantId, score) {
12494
+ const dispatchKey = this.keys.dispatchKey(shardId);
12495
+ await this.redis.zadd(dispatchKey, score, tenantId);
12496
+ }
12497
+ /**
12498
+ * Remove a queue from a tenant's queue index.
12499
+ */
12500
+ async removeQueueFromTenant(tenantId, queueId) {
12501
+ const tenantQueueKey = this.keys.tenantQueueIndexKey(tenantId);
12502
+ await this.redis.zrem(tenantQueueKey, queueId);
12503
+ }
12504
+ /**
12505
+ * Close the Redis connection.
12506
+ */
12507
+ async close() {
12508
+ await this.redis.quit();
12509
+ }
12510
+ };
12155
12511
 
12156
12512
  // src/fair-queue/telemetry.ts
12157
12513
  var FairQueueAttributes = {
@@ -12333,6 +12689,18 @@ var FairQueueTelemetry = class {
12333
12689
  }
12334
12690
  });
12335
12691
  }
12692
+ if (callbacks.getDispatchLength && callbacks.shardCount) {
12693
+ const getDispatchLength = callbacks.getDispatchLength;
12694
+ const shardCount = callbacks.shardCount;
12695
+ this.metrics.dispatchLength.addCallback(async (observableResult) => {
12696
+ for (let shardId = 0; shardId < shardCount; shardId++) {
12697
+ const length = await getDispatchLength(shardId);
12698
+ observableResult.observe(length, {
12699
+ [FairQueueAttributes.SHARD_ID]: shardId.toString()
12700
+ });
12701
+ }
12702
+ });
12703
+ }
12336
12704
  if (callbacks.getInflightCount && callbacks.shardCount) {
12337
12705
  const getInflightCount = callbacks.getInflightCount;
12338
12706
  const shardCount = callbacks.shardCount;
@@ -12435,9 +12803,13 @@ var FairQueueTelemetry = class {
12435
12803
  unit: "messages"
12436
12804
  }),
12437
12805
  masterQueueLength: this.meter.createObservableGauge(`${this.name}.master_queue.length`, {
12438
- description: "Number of queues in master queue shard",
12806
+ description: "Number of queues in legacy master queue shard (draining)",
12439
12807
  unit: "queues"
12440
12808
  }),
12809
+ dispatchLength: this.meter.createObservableGauge(`${this.name}.dispatch.length`, {
12810
+ description: "Number of tenants in dispatch index shard",
12811
+ unit: "tenants"
12812
+ }),
12441
12813
  inflightCount: this.meter.createObservableGauge(`${this.name}.inflight.count`, {
12442
12814
  description: "Number of messages currently being processed",
12443
12815
  unit: "messages"
@@ -12841,10 +13213,12 @@ var VisibilityManager = class {
12841
13213
  * @param queueId - The queue ID
12842
13214
  * @param queueKey - The Redis key for the queue
12843
13215
  * @param queueItemsKey - The Redis key for the queue items hash
12844
- * @param masterQueueKey - The Redis key for the master queue
13216
+ * @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
13217
+ * @param dispatchKey - The Redis key for the dispatch index (Level 1)
13218
+ * @param tenantId - The tenant ID
12845
13219
  * @param score - Optional score for the message (defaults to now)
12846
13220
  */
12847
- async release(messageId, queueId, queueKey, queueItemsKey, masterQueueKey, score) {
13221
+ async release(messageId, queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, tenantId, score, updatedData) {
12848
13222
  const shardId = this.#getShardForQueue(queueId);
12849
13223
  const inflightKey = this.keys.inflightKey(shardId);
12850
13224
  const inflightDataKey = this.keys.inflightDataKey(shardId);
@@ -12855,11 +13229,14 @@ var VisibilityManager = class {
12855
13229
  inflightDataKey,
12856
13230
  queueKey,
12857
13231
  queueItemsKey,
12858
- masterQueueKey,
13232
+ tenantQueueIndexKey,
13233
+ dispatchKey,
12859
13234
  member,
12860
13235
  messageId,
12861
13236
  messageScore.toString(),
12862
- queueId
13237
+ queueId,
13238
+ updatedData ?? "",
13239
+ tenantId
12863
13240
  );
12864
13241
  this.logger.debug("Message released", {
12865
13242
  messageId,
@@ -12876,10 +13253,12 @@ var VisibilityManager = class {
12876
13253
  * @param queueId - The queue ID
12877
13254
  * @param queueKey - The Redis key for the queue
12878
13255
  * @param queueItemsKey - The Redis key for the queue items hash
12879
- * @param masterQueueKey - The Redis key for the master queue
13256
+ * @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
13257
+ * @param dispatchKey - The Redis key for the dispatch index (Level 1)
13258
+ * @param tenantId - The tenant ID
12880
13259
  * @param score - Optional score for the messages (defaults to now)
12881
13260
  */
12882
- async releaseBatch(messages, queueId, queueKey, queueItemsKey, masterQueueKey, score) {
13261
+ async releaseBatch(messages, queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, tenantId, score) {
12883
13262
  if (messages.length === 0) {
12884
13263
  return;
12885
13264
  }
@@ -12894,9 +13273,11 @@ var VisibilityManager = class {
12894
13273
  inflightDataKey,
12895
13274
  queueKey,
12896
13275
  queueItemsKey,
12897
- masterQueueKey,
13276
+ tenantQueueIndexKey,
13277
+ dispatchKey,
12898
13278
  messageScore.toString(),
12899
13279
  queueId,
13280
+ tenantId,
12900
13281
  ...members,
12901
13282
  ...messageIds
12902
13283
  );
@@ -12936,7 +13317,7 @@ var VisibilityManager = class {
12936
13317
  continue;
12937
13318
  }
12938
13319
  const { messageId, queueId } = this.#parseMember(member);
12939
- const { queueKey, queueItemsKey, masterQueueKey } = getQueueKeys(queueId);
13320
+ const { queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, tenantId } = getQueueKeys(queueId);
12940
13321
  try {
12941
13322
  const dataJson = await this.redis.hget(inflightDataKey, messageId);
12942
13323
  let storedMessage = null;
@@ -12952,11 +13333,14 @@ var VisibilityManager = class {
12952
13333
  inflightDataKey,
12953
13334
  queueKey,
12954
13335
  queueItemsKey,
12955
- masterQueueKey,
13336
+ tenantQueueIndexKey,
13337
+ dispatchKey,
12956
13338
  member,
12957
13339
  messageId,
12958
13340
  score.toString(),
12959
- queueId
13341
+ queueId,
13342
+ "",
13343
+ tenantId
12960
13344
  );
12961
13345
  if (storedMessage) {
12962
13346
  reclaimedMessages.push({
@@ -13157,18 +13541,21 @@ return results
13157
13541
  `
13158
13542
  });
13159
13543
  this.redis.defineCommand("releaseMessage", {
13160
- numberOfKeys: 5,
13544
+ numberOfKeys: 6,
13161
13545
  lua: `
13162
13546
  local inflightKey = KEYS[1]
13163
13547
  local inflightDataKey = KEYS[2]
13164
13548
  local queueKey = KEYS[3]
13165
13549
  local queueItemsKey = KEYS[4]
13166
- local masterQueueKey = KEYS[5]
13550
+ local tenantQueueIndexKey = KEYS[5]
13551
+ local dispatchKey = KEYS[6]
13167
13552
 
13168
13553
  local member = ARGV[1]
13169
13554
  local messageId = ARGV[2]
13170
13555
  local score = tonumber(ARGV[3])
13171
13556
  local queueId = ARGV[4]
13557
+ local updatedData = ARGV[5]
13558
+ local tenantId = ARGV[6]
13172
13559
 
13173
13560
  -- Get message data from in-flight
13174
13561
  local payload = redis.call('HGET', inflightDataKey, messageId)
@@ -13177,6 +13564,12 @@ if not payload then
13177
13564
  return 0
13178
13565
  end
13179
13566
 
13567
+ -- Use updatedData if provided (e.g. incremented attempt count for retries),
13568
+ -- otherwise use the original in-flight data
13569
+ if updatedData and updatedData ~= "" then
13570
+ payload = updatedData
13571
+ end
13572
+
13180
13573
  -- Remove from in-flight
13181
13574
  redis.call('ZREM', inflightKey, member)
13182
13575
  redis.call('HDEL', inflightDataKey, messageId)
@@ -13185,33 +13578,39 @@ redis.call('HDEL', inflightDataKey, messageId)
13185
13578
  redis.call('ZADD', queueKey, score, messageId)
13186
13579
  redis.call('HSET', queueItemsKey, messageId, payload)
13187
13580
 
13188
- -- Update master queue with oldest message timestamp
13189
- -- This ensures delayed messages don't push the queue priority to the future
13190
- -- when there are other ready messages in the queue
13581
+ -- Update tenant queue index (Level 2) with queue's oldest message
13191
13582
  local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
13192
13583
  if #oldest >= 2 then
13193
- redis.call('ZADD', masterQueueKey, oldest[2], queueId)
13584
+ redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
13585
+ end
13586
+
13587
+ -- Update dispatch index (Level 1) with tenant's oldest across all queues
13588
+ local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
13589
+ if #tenantOldest >= 2 then
13590
+ redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
13194
13591
  end
13195
13592
 
13196
13593
  return 1
13197
13594
  `
13198
13595
  });
13199
13596
  this.redis.defineCommand("releaseMessageBatch", {
13200
- numberOfKeys: 5,
13597
+ numberOfKeys: 6,
13201
13598
  lua: `
13202
13599
  local inflightKey = KEYS[1]
13203
13600
  local inflightDataKey = KEYS[2]
13204
13601
  local queueKey = KEYS[3]
13205
13602
  local queueItemsKey = KEYS[4]
13206
- local masterQueueKey = KEYS[5]
13603
+ local tenantQueueIndexKey = KEYS[5]
13604
+ local dispatchKey = KEYS[6]
13207
13605
 
13208
13606
  local score = tonumber(ARGV[1])
13209
13607
  local queueId = ARGV[2]
13608
+ local tenantId = ARGV[3]
13210
13609
 
13211
13610
  -- Remaining args are: members..., messageIds...
13212
13611
  -- Calculate how many messages we have
13213
- local numMessages = (table.getn(ARGV) - 2) / 2
13214
- local membersStart = 3
13612
+ local numMessages = (table.getn(ARGV) - 3) / 2
13613
+ local membersStart = 4
13215
13614
  local messageIdsStart = membersStart + numMessages
13216
13615
 
13217
13616
  local releasedCount = 0
@@ -13219,27 +13618,33 @@ local releasedCount = 0
13219
13618
  for i = 0, numMessages - 1 do
13220
13619
  local member = ARGV[membersStart + i]
13221
13620
  local messageId = ARGV[messageIdsStart + i]
13222
-
13621
+
13223
13622
  -- Get message data from in-flight
13224
13623
  local payload = redis.call('HGET', inflightDataKey, messageId)
13225
13624
  if payload then
13226
13625
  -- Remove from in-flight
13227
13626
  redis.call('ZREM', inflightKey, member)
13228
13627
  redis.call('HDEL', inflightDataKey, messageId)
13229
-
13628
+
13230
13629
  -- Add back to queue
13231
13630
  redis.call('ZADD', queueKey, score, messageId)
13232
13631
  redis.call('HSET', queueItemsKey, messageId, payload)
13233
-
13632
+
13234
13633
  releasedCount = releasedCount + 1
13235
13634
  end
13236
13635
  end
13237
13636
 
13238
- -- Update master queue with oldest message timestamp (only once at the end)
13637
+ -- Update dispatch indexes (only once at the end)
13239
13638
  if releasedCount > 0 then
13639
+ -- Update tenant queue index (Level 2)
13240
13640
  local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
13241
13641
  if #oldest >= 2 then
13242
- redis.call('ZADD', masterQueueKey, oldest[2], queueId)
13642
+ redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
13643
+ end
13644
+ -- Update dispatch index (Level 1)
13645
+ local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
13646
+ if #tenantOldest >= 2 then
13647
+ redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
13243
13648
  end
13244
13649
  end
13245
13650
 
@@ -13527,6 +13932,15 @@ var DefaultFairQueueKeyProducer = class {
13527
13932
  return this.#buildKey("worker", consumerId);
13528
13933
  }
13529
13934
  // ============================================================================
13935
+ // Tenant Dispatch Keys (Two-Level Index)
13936
+ // ============================================================================
13937
+ dispatchKey(shardId) {
13938
+ return this.#buildKey("dispatch", shardId.toString());
13939
+ }
13940
+ tenantQueueIndexKey(tenantId) {
13941
+ return this.#buildKey("tenantq", tenantId);
13942
+ }
13943
+ // ============================================================================
13530
13944
  // Dead Letter Queue Keys
13531
13945
  // ============================================================================
13532
13946
  deadLetterQueueKey(tenantId) {
@@ -13739,6 +14153,44 @@ var DRRScheduler = class extends BaseScheduler {
13739
14153
  queues: t.queues
13740
14154
  }));
13741
14155
  }
14156
+ /**
14157
+ * Select queues using the two-level tenant dispatch index.
14158
+ *
14159
+ * Algorithm:
14160
+ * 1. ZRANGEBYSCORE on dispatch index (gets only tenants with queues - much smaller)
14161
+ * 2. Add quantum to each tenant's deficit (atomically)
14162
+ * 3. Check capacity as safety net (dispatch should only have tenants with capacity)
14163
+ * 4. Select tenants with deficit >= 1, sorted by deficit (highest first)
14164
+ * 5. For each tenant, fetch their queues from Level 2 index
14165
+ */
14166
+ async selectQueuesFromDispatch(dispatchShardKey, consumerId, context2) {
14167
+ const tenants = await this.#getTenantsFromDispatch(dispatchShardKey);
14168
+ if (tenants.length === 0) {
14169
+ return [];
14170
+ }
14171
+ const tenantIds = tenants.map((t) => t.tenantId);
14172
+ const deficits = await this.#addQuantumToTenants(tenantIds);
14173
+ const candidates = tenantIds.map((tenantId, index) => ({ tenantId, deficit: deficits[index] ?? 0 })).filter((t) => t.deficit >= 1);
14174
+ candidates.sort((a, b) => b.deficit - a.deficit);
14175
+ for (const { tenantId, deficit } of candidates) {
14176
+ const isAtCapacity = await context2.isAtCapacity("tenant", tenantId);
14177
+ if (isAtCapacity) continue;
14178
+ const queueLimit = Math.ceil(deficit);
14179
+ const queues = await context2.getQueuesForTenant(tenantId, queueLimit);
14180
+ if (queues.length > 0) {
14181
+ this.logger.debug("DRR dispatch: selected tenant", {
14182
+ dispatchTenants: tenants.length,
14183
+ candidates: candidates.length,
14184
+ selectedTenant: tenantId,
14185
+ deficit,
14186
+ queueLimit,
14187
+ queuesReturned: queues.length
14188
+ });
14189
+ return [{ tenantId, queues: queues.map((q) => q.queueId) }];
14190
+ }
14191
+ }
14192
+ return [];
14193
+ }
13742
14194
  /**
13743
14195
  * Record that a message was processed from a tenant.
13744
14196
  * Decrements the tenant's deficit.
@@ -13793,6 +14245,30 @@ var DRRScheduler = class extends BaseScheduler {
13793
14245
  #deficitKey() {
13794
14246
  return `${this.keys.masterQueueKey(0).split(":")[0]}:drr:deficit`;
13795
14247
  }
14248
+ async #getTenantsFromDispatch(dispatchKey) {
14249
+ const now = Date.now();
14250
+ const results = await this.redis.zrangebyscore(
14251
+ dispatchKey,
14252
+ "-inf",
14253
+ now,
14254
+ "WITHSCORES",
14255
+ "LIMIT",
14256
+ 0,
14257
+ this.masterQueueLimit
14258
+ );
14259
+ const tenants = [];
14260
+ for (let i = 0; i < results.length; i += 2) {
14261
+ const tenantId = results[i];
14262
+ const scoreStr = results[i + 1];
14263
+ if (tenantId && scoreStr) {
14264
+ tenants.push({
14265
+ tenantId,
14266
+ score: parseFloat(scoreStr)
14267
+ });
14268
+ }
14269
+ }
14270
+ return tenants;
14271
+ }
13796
14272
  async #getQueuesFromShard(shardKey) {
13797
14273
  const now = Date.now();
13798
14274
  const results = await this.redis.zrangebyscore(
@@ -14426,7 +14902,8 @@ var FairQueue = class {
14426
14902
  this.cooloffThreshold = options.cooloff?.threshold ?? 10;
14427
14903
  this.cooloffPeriodMs = options.cooloff?.periodMs ?? 1e4;
14428
14904
  this.maxCooloffStatesSize = options.cooloff?.maxStatesSize ?? 1e3;
14429
- this.globalRateLimiter = options.globalRateLimiter;
14905
+ this.workerQueueMaxDepth = options.workerQueueMaxDepth ?? 0;
14906
+ this.workerQueueDepthCheckId = options.workerQueueDepthCheckId;
14430
14907
  this.consumerTraceMaxIterations = options.consumerTraceMaxIterations ?? 500;
14431
14908
  this.consumerTraceTimeoutSeconds = options.consumerTraceTimeoutSeconds ?? 60;
14432
14909
  this.telemetry = new FairQueueTelemetry({
@@ -14449,6 +14926,11 @@ var FairQueue = class {
14449
14926
  keys: options.keys,
14450
14927
  shardCount: this.shardCount
14451
14928
  });
14929
+ this.tenantDispatch = new TenantDispatch({
14930
+ redis: options.redis,
14931
+ keys: options.keys,
14932
+ shardCount: this.shardCount
14933
+ });
14452
14934
  if (options.concurrencyGroups && options.concurrencyGroups.length > 0) {
14453
14935
  this.concurrencyManager = new ConcurrencyManager({
14454
14936
  redis: options.redis,
@@ -14507,8 +14989,9 @@ var FairQueue = class {
14507
14989
  cooloffPeriodMs;
14508
14990
  maxCooloffStatesSize;
14509
14991
  queueCooloffStates = /* @__PURE__ */ new Map();
14510
- // Global rate limiter
14511
- globalRateLimiter;
14992
+ // Worker queue backpressure
14993
+ workerQueueMaxDepth;
14994
+ workerQueueDepthCheckId;
14512
14995
  // Consumer tracing
14513
14996
  consumerTraceMaxIterations;
14514
14997
  consumerTraceTimeoutSeconds;
@@ -14520,6 +15003,8 @@ var FairQueue = class {
14520
15003
  reclaimLoop;
14521
15004
  // Queue descriptor cache for message processing
14522
15005
  queueDescriptorCache = /* @__PURE__ */ new Map();
15006
+ // Two-level tenant dispatch
15007
+ tenantDispatch;
14523
15008
  // ============================================================================
14524
15009
  // Public API - Telemetry
14525
15010
  // ============================================================================
@@ -14534,6 +15019,9 @@ var FairQueue = class {
14534
15019
  getMasterQueueLength: async (shardId) => {
14535
15020
  return await this.masterQueue.getShardQueueCount(shardId);
14536
15021
  },
15022
+ getDispatchLength: async (shardId) => {
15023
+ return await this.tenantDispatch.getShardTenantCount(shardId);
15024
+ },
14537
15025
  getInflightCount: async (shardId) => {
14538
15026
  return await this.visibilityManager.getInflightCount(shardId);
14539
15027
  },
@@ -14558,8 +15046,9 @@ var FairQueue = class {
14558
15046
  const timestamp = options.timestamp ?? Date.now();
14559
15047
  const queueKey = this.keys.queueKey(options.queueId);
14560
15048
  const queueItemsKey = this.keys.queueItemsKey(options.queueId);
14561
- const shardId = this.masterQueue.getShardForQueue(options.queueId);
14562
- const masterQueueKey = this.keys.masterQueueKey(shardId);
15049
+ const dispatchShardId = this.tenantDispatch.getShardForTenant(options.tenantId);
15050
+ const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(options.tenantId);
15051
+ const dispatchKey = this.keys.dispatchKey(dispatchShardId);
14563
15052
  if (this.validateOnEnqueue && this.payloadSchema) {
14564
15053
  const result = this.payloadSchema.safeParse(options.payload);
14565
15054
  if (!result.success) {
@@ -14590,20 +15079,22 @@ var FairQueue = class {
14590
15079
  }) : void 0,
14591
15080
  metadata: options.metadata
14592
15081
  };
14593
- await this.redis.enqueueMessageAtomic(
15082
+ await this.redis.enqueueMessageAtomicV2(
14594
15083
  queueKey,
14595
15084
  queueItemsKey,
14596
- masterQueueKey,
15085
+ tenantQueueIndexKey,
15086
+ dispatchKey,
14597
15087
  options.queueId,
14598
15088
  messageId,
14599
15089
  timestamp.toString(),
14600
- JSON.stringify(storedMessage)
15090
+ JSON.stringify(storedMessage),
15091
+ options.tenantId
14601
15092
  );
14602
15093
  span.setAttributes({
14603
15094
  [FairQueueAttributes.QUEUE_ID]: options.queueId,
14604
15095
  [FairQueueAttributes.TENANT_ID]: options.tenantId,
14605
15096
  [FairQueueAttributes.MESSAGE_ID]: messageId,
14606
- [FairQueueAttributes.SHARD_ID]: shardId.toString()
15097
+ [FairQueueAttributes.SHARD_ID]: dispatchShardId.toString()
14607
15098
  });
14608
15099
  this.telemetry.recordEnqueue();
14609
15100
  this.logger.debug("Message enqueued", {
@@ -14630,8 +15121,9 @@ var FairQueue = class {
14630
15121
  async (span) => {
14631
15122
  const queueKey = this.keys.queueKey(options.queueId);
14632
15123
  const queueItemsKey = this.keys.queueItemsKey(options.queueId);
14633
- const shardId = this.masterQueue.getShardForQueue(options.queueId);
14634
- const masterQueueKey = this.keys.masterQueueKey(shardId);
15124
+ const dispatchShardId = this.tenantDispatch.getShardForTenant(options.tenantId);
15125
+ const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(options.tenantId);
15126
+ const dispatchKey = this.keys.dispatchKey(dispatchShardId);
14635
15127
  const now = Date.now();
14636
15128
  const descriptor = {
14637
15129
  id: options.queueId,
@@ -14673,18 +15165,20 @@ var FairQueue = class {
14673
15165
  messageIds.push(messageId);
14674
15166
  args.push(messageId, timestamp.toString(), JSON.stringify(storedMessage));
14675
15167
  }
14676
- await this.redis.enqueueBatchAtomic(
15168
+ await this.redis.enqueueBatchAtomicV2(
14677
15169
  queueKey,
14678
15170
  queueItemsKey,
14679
- masterQueueKey,
15171
+ tenantQueueIndexKey,
15172
+ dispatchKey,
14680
15173
  options.queueId,
15174
+ options.tenantId,
14681
15175
  ...args
14682
15176
  );
14683
15177
  span.setAttributes({
14684
15178
  [FairQueueAttributes.QUEUE_ID]: options.queueId,
14685
15179
  [FairQueueAttributes.TENANT_ID]: options.tenantId,
14686
15180
  [FairQueueAttributes.MESSAGE_COUNT]: messageIds.length,
14687
- [FairQueueAttributes.SHARD_ID]: shardId.toString()
15181
+ [FairQueueAttributes.SHARD_ID]: dispatchShardId.toString()
14688
15182
  });
14689
15183
  this.telemetry.recordEnqueueBatch(messageIds.length);
14690
15184
  this.logger.debug("Batch enqueued", {
@@ -14886,6 +15380,7 @@ var FairQueue = class {
14886
15380
  this.batchedSpanManager.cleanupAll();
14887
15381
  await Promise.all([
14888
15382
  this.masterQueue.close(),
15383
+ this.tenantDispatch.close(),
14889
15384
  this.concurrencyManager?.close(),
14890
15385
  this.visibilityManager.close(),
14891
15386
  this.workerQueueManager.close(),
@@ -14904,10 +15399,14 @@ var FairQueue = class {
14904
15399
  return await this.redis.zcard(queueKey);
14905
15400
  }
14906
15401
  /**
14907
- * Get total queue count across all shards.
15402
+ * Get total tenant count across dispatch shards plus any legacy queues still draining.
14908
15403
  */
14909
15404
  async getTotalQueueCount() {
14910
- return await this.masterQueue.getTotalQueueCount();
15405
+ const [dispatchCount, legacyCount] = await Promise.all([
15406
+ this.tenantDispatch.getTotalTenantCount(),
15407
+ this.masterQueue.getTotalQueueCount()
15408
+ ]);
15409
+ return dispatchCount + legacyCount;
14911
15410
  }
14912
15411
  /**
14913
15412
  * Get total in-flight message count.
@@ -14938,7 +15437,7 @@ var FairQueue = class {
14938
15437
  loopId,
14939
15438
  async (span) => {
14940
15439
  span.setAttribute("shard_id", shardId);
14941
- return await this.#processMasterQueueShard(loopId, shardId, span);
15440
+ return await this.#processShardIteration(loopId, shardId, span);
14942
15441
  },
14943
15442
  {
14944
15443
  iterationSpanName: "processMasterQueueShard",
@@ -14977,32 +15476,142 @@ var FairQueue = class {
14977
15476
  this.batchedSpanManager.cleanup(loopId);
14978
15477
  }
14979
15478
  }
14980
- async #processMasterQueueShard(loopId, shardId, parentSpan) {
15479
+ /**
15480
+ * Process a shard iteration. Runs both the new tenant dispatch path
15481
+ * and the legacy master queue drain path.
15482
+ */
15483
+ async #processShardIteration(loopId, shardId, parentSpan) {
15484
+ let hadWork = false;
15485
+ hadWork = await this.#processDispatchShard(loopId, shardId, parentSpan);
15486
+ const legacyCount = await this.masterQueue.getShardQueueCount(shardId);
15487
+ if (legacyCount > 0) {
15488
+ const drainHadWork = await this.#drainLegacyMasterQueueShard(loopId, shardId, parentSpan);
15489
+ hadWork = hadWork || drainHadWork;
15490
+ }
15491
+ return hadWork;
15492
+ }
15493
+ /**
15494
+ * Main path: process queues using the two-level tenant dispatch index.
15495
+ * Level 1: dispatch index → tenantIds. Level 2: per-tenant → queueIds.
15496
+ */
15497
+ async #processDispatchShard(loopId, shardId, parentSpan) {
15498
+ const dispatchKey = this.keys.dispatchKey(shardId);
15499
+ const dispatchSize = await this.tenantDispatch.getShardTenantCount(shardId);
15500
+ parentSpan?.setAttribute("dispatch_size", dispatchSize);
15501
+ this.batchedSpanManager.incrementStat(loopId, "dispatch_size_sum", dispatchSize);
15502
+ const schedulerContext = {
15503
+ ...this.#createSchedulerContext(),
15504
+ getQueuesForTenant: async (tenantId, limit) => {
15505
+ return this.tenantDispatch.getQueuesForTenant(tenantId, limit);
15506
+ }
15507
+ };
15508
+ let tenantQueues;
15509
+ if (this.scheduler.selectQueuesFromDispatch) {
15510
+ tenantQueues = await this.telemetry.trace(
15511
+ "selectQueuesFromDispatch",
15512
+ async (span) => {
15513
+ span.setAttribute(FairQueueAttributes.SHARD_ID, shardId.toString());
15514
+ span.setAttribute(FairQueueAttributes.CONSUMER_ID, loopId);
15515
+ span.setAttribute("dispatch_size", dispatchSize);
15516
+ const result = await this.scheduler.selectQueuesFromDispatch(
15517
+ dispatchKey,
15518
+ loopId,
15519
+ schedulerContext
15520
+ );
15521
+ span.setAttribute("tenant_count", result.length);
15522
+ span.setAttribute(
15523
+ "queue_count",
15524
+ result.reduce((acc, t) => acc + t.queues.length, 0)
15525
+ );
15526
+ return result;
15527
+ },
15528
+ { kind: SpanKind.INTERNAL }
15529
+ );
15530
+ } else {
15531
+ tenantQueues = await this.#fallbackDispatchToLegacyScheduler(
15532
+ loopId,
15533
+ shardId,
15534
+ schedulerContext,
15535
+ parentSpan
15536
+ );
15537
+ }
15538
+ if (tenantQueues.length === 0) {
15539
+ this.batchedSpanManager.incrementStat(loopId, "empty_iterations");
15540
+ return false;
15541
+ }
15542
+ return this.#processSelectedQueues(loopId, shardId, tenantQueues);
15543
+ }
15544
+ /**
15545
+ * Drain path: process remaining messages from the legacy master queue shard.
15546
+ * Uses simple ZRANGEBYSCORE without DRR - just flushing pre-deploy messages.
15547
+ */
15548
+ async #drainLegacyMasterQueueShard(loopId, shardId, parentSpan) {
14981
15549
  const masterQueueKey = this.keys.masterQueueKey(shardId);
14982
- const masterQueueSize = await this.masterQueue.getShardQueueCount(shardId);
14983
- parentSpan?.setAttribute("master_queue_size", masterQueueSize);
14984
- this.batchedSpanManager.incrementStat(loopId, "master_queue_size_sum", masterQueueSize);
14985
- const schedulerContext = this.#createSchedulerContext();
14986
- const tenantQueues = await this.telemetry.trace(
14987
- "selectQueues",
14988
- async (span) => {
14989
- span.setAttribute(FairQueueAttributes.SHARD_ID, shardId.toString());
14990
- span.setAttribute(FairQueueAttributes.CONSUMER_ID, loopId);
14991
- span.setAttribute("master_queue_size", masterQueueSize);
14992
- const result = await this.scheduler.selectQueues(masterQueueKey, loopId, schedulerContext);
14993
- span.setAttribute("tenant_count", result.length);
14994
- span.setAttribute(
14995
- "queue_count",
14996
- result.reduce((acc, t) => acc + t.queues.length, 0)
14997
- );
14998
- return result;
14999
- },
15000
- { kind: SpanKind.INTERNAL }
15550
+ const now = Date.now();
15551
+ const results = await this.redis.zrangebyscore(
15552
+ masterQueueKey,
15553
+ "-inf",
15554
+ now,
15555
+ "WITHSCORES",
15556
+ "LIMIT",
15557
+ 0,
15558
+ 100
15001
15559
  );
15560
+ if (results.length === 0) {
15561
+ return false;
15562
+ }
15563
+ const byTenant = /* @__PURE__ */ new Map();
15564
+ for (let i = 0; i < results.length; i += 2) {
15565
+ const queueId = results[i];
15566
+ const _score = results[i + 1];
15567
+ if (queueId && _score) {
15568
+ const tenantId = this.keys.extractTenantId(queueId);
15569
+ const existing = byTenant.get(tenantId) ?? [];
15570
+ existing.push(queueId);
15571
+ byTenant.set(tenantId, existing);
15572
+ }
15573
+ }
15574
+ const tenantQueues = [];
15575
+ for (const [tenantId, queueIds] of byTenant) {
15576
+ if (this.concurrencyManager) {
15577
+ const atCapacity = await this.concurrencyManager.isAtCapacity("tenant", tenantId);
15578
+ if (atCapacity) continue;
15579
+ }
15580
+ tenantQueues.push({ tenantId, queues: queueIds });
15581
+ }
15002
15582
  if (tenantQueues.length === 0) {
15003
- this.batchedSpanManager.incrementStat(loopId, "empty_iterations");
15004
15583
  return false;
15005
15584
  }
15585
+ parentSpan?.setAttribute("drain_tenants", tenantQueues.length);
15586
+ this.batchedSpanManager.incrementStat(loopId, "drain_tenants", tenantQueues.length);
15587
+ return this.#processSelectedQueues(loopId, shardId, tenantQueues);
15588
+ }
15589
+ /**
15590
+ * Fallback for schedulers that don't implement selectQueuesFromDispatch.
15591
+ * Reads dispatch index, fetches per-tenant queues, groups by tenant,
15592
+ * and filters at-capacity tenants. No DRR deficit tracking in this path.
15593
+ */
15594
+ async #fallbackDispatchToLegacyScheduler(loopId, shardId, context2, parentSpan) {
15595
+ const tenants = await this.tenantDispatch.getTenantsFromShard(shardId);
15596
+ if (tenants.length === 0) return [];
15597
+ const tenantQueues = [];
15598
+ for (const { tenantId } of tenants) {
15599
+ if (this.concurrencyManager) {
15600
+ const atCapacity = await this.concurrencyManager.isAtCapacity("tenant", tenantId);
15601
+ if (atCapacity) continue;
15602
+ }
15603
+ const queues = await this.tenantDispatch.getQueuesForTenant(tenantId);
15604
+ if (queues.length > 0) {
15605
+ tenantQueues.push({ tenantId, queues: queues.map((q) => q.queueId) });
15606
+ }
15607
+ }
15608
+ return tenantQueues;
15609
+ }
15610
+ /**
15611
+ * Shared claim loop: process selected queues from either dispatch or drain path.
15612
+ * Claims messages and pushes to worker queues.
15613
+ */
15614
+ async #processSelectedQueues(loopId, shardId, tenantQueues) {
15006
15615
  this.batchedSpanManager.incrementStat(loopId, "tenants_selected", tenantQueues.length);
15007
15616
  this.batchedSpanManager.incrementStat(
15008
15617
  loopId,
@@ -15069,10 +15678,10 @@ var FairQueue = class {
15069
15678
  }
15070
15679
  return messagesProcessed > 0;
15071
15680
  }
15072
- async #claimAndPushToWorkerQueue(loopId, queueId, tenantId, shardId) {
15681
+ async #claimAndPushToWorkerQueue(loopId, queueId, tenantId, _consumerShardId) {
15682
+ const dispatchShardId = this.tenantDispatch.getShardForTenant(tenantId);
15073
15683
  const queueKey = this.keys.queueKey(queueId);
15074
15684
  const queueItemsKey = this.keys.queueItemsKey(queueId);
15075
- const masterQueueKey = this.keys.masterQueueKey(shardId);
15076
15685
  const descriptor = this.queueDescriptorCache.get(queueId) ?? {
15077
15686
  id: queueId,
15078
15687
  tenantId,
@@ -15082,28 +15691,23 @@ var FairQueue = class {
15082
15691
  if (this.concurrencyManager) {
15083
15692
  const availableCapacity = await this.concurrencyManager.getAvailableCapacity(descriptor);
15084
15693
  if (availableCapacity === 0) {
15085
- this.#incrementCooloff(queueId);
15086
15694
  return 0;
15087
15695
  }
15088
15696
  maxClaimCount = Math.min(maxClaimCount, availableCapacity);
15089
15697
  }
15090
- if (this.globalRateLimiter) {
15091
- const result = await this.globalRateLimiter.limit();
15092
- if (!result.allowed && result.resetAt) {
15093
- const waitMs = Math.max(0, result.resetAt - Date.now());
15094
- if (waitMs > 0) {
15095
- this.logger.debug("Global rate limit reached, waiting", { waitMs, loopId });
15096
- await new Promise((resolve) => setTimeout(resolve, waitMs));
15097
- }
15698
+ if (this.workerQueueMaxDepth > 0 && this.workerQueueDepthCheckId) {
15699
+ const depth = await this.workerQueueManager.getLength(this.workerQueueDepthCheckId);
15700
+ if (depth >= this.workerQueueMaxDepth) {
15701
+ return 0;
15098
15702
  }
15703
+ const remainingCapacity = this.workerQueueMaxDepth - depth;
15704
+ maxClaimCount = Math.min(maxClaimCount, remainingCapacity);
15099
15705
  }
15100
15706
  const claimedMessages = await this.visibilityManager.claimBatch(queueId, queueKey, queueItemsKey, loopId, maxClaimCount, this.visibilityTimeoutMs);
15101
15707
  if (claimedMessages.length === 0) {
15102
- const removed = await this.redis.updateMasterQueueIfEmpty(masterQueueKey, queueKey, queueId);
15103
- if (removed === 1) {
15104
- this.queueDescriptorCache.delete(queueId);
15105
- this.queueCooloffStates.delete(queueId);
15106
- }
15708
+ await this.#updateAllIndexesAfterDequeue(queueId, tenantId);
15709
+ this.queueDescriptorCache.delete(queueId);
15710
+ this.queueCooloffStates.delete(queueId);
15107
15711
  return 0;
15108
15712
  }
15109
15713
  let processedCount = 0;
@@ -15112,12 +15716,16 @@ var FairQueue = class {
15112
15716
  if (this.concurrencyManager) {
15113
15717
  const reserved = await this.concurrencyManager.reserve(descriptor, message.messageId);
15114
15718
  if (!reserved) {
15719
+ const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(tenantId);
15720
+ const dispatchKey = this.keys.dispatchKey(dispatchShardId);
15115
15721
  await this.visibilityManager.releaseBatch(
15116
15722
  claimedMessages.slice(i),
15117
15723
  queueId,
15118
15724
  queueKey,
15119
15725
  queueItemsKey,
15120
- masterQueueKey
15726
+ tenantQueueIndexKey,
15727
+ dispatchKey,
15728
+ tenantId
15121
15729
  );
15122
15730
  break;
15123
15731
  }
@@ -15177,8 +15785,6 @@ var FairQueue = class {
15177
15785
  */
15178
15786
  async completeMessage(messageId, queueId) {
15179
15787
  const shardId = this.masterQueue.getShardForQueue(queueId);
15180
- const queueKey = this.keys.queueKey(queueId);
15181
- const masterQueueKey = this.keys.masterQueueKey(shardId);
15182
15788
  const inflightDataKey = this.keys.inflightDataKey(shardId);
15183
15789
  const dataJson = await this.redis.hget(inflightDataKey, messageId);
15184
15790
  let storedMessage = null;
@@ -15192,13 +15798,16 @@ var FairQueue = class {
15192
15798
  id: queueId,
15193
15799
  tenantId: storedMessage.tenantId,
15194
15800
  metadata: storedMessage.metadata ?? {}
15195
- } : { id: queueId, tenantId: "", metadata: {} };
15801
+ } : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} };
15196
15802
  await this.visibilityManager.complete(messageId, queueId);
15197
15803
  if (this.concurrencyManager && storedMessage) {
15198
15804
  await this.concurrencyManager.release(descriptor, messageId);
15199
15805
  }
15200
- const removed = await this.redis.updateMasterQueueIfEmpty(masterQueueKey, queueKey, queueId);
15201
- if (removed === 1) {
15806
+ const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(
15807
+ queueId,
15808
+ descriptor.tenantId
15809
+ );
15810
+ if (queueEmpty) {
15202
15811
  this.queueDescriptorCache.delete(queueId);
15203
15812
  this.queueCooloffStates.delete(queueId);
15204
15813
  }
@@ -15219,7 +15828,6 @@ var FairQueue = class {
15219
15828
  const shardId = this.masterQueue.getShardForQueue(queueId);
15220
15829
  const queueKey = this.keys.queueKey(queueId);
15221
15830
  const queueItemsKey = this.keys.queueItemsKey(queueId);
15222
- const masterQueueKey = this.keys.masterQueueKey(shardId);
15223
15831
  const inflightDataKey = this.keys.inflightDataKey(shardId);
15224
15832
  const dataJson = await this.redis.hget(inflightDataKey, messageId);
15225
15833
  let storedMessage = null;
@@ -15233,13 +15841,18 @@ var FairQueue = class {
15233
15841
  id: queueId,
15234
15842
  tenantId: storedMessage.tenantId,
15235
15843
  metadata: storedMessage.metadata ?? {}
15236
- } : { id: queueId, tenantId: "", metadata: {} };
15844
+ } : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} };
15845
+ const dispatchShardId = this.tenantDispatch.getShardForTenant(descriptor.tenantId);
15846
+ const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(descriptor.tenantId);
15847
+ const dispatchKey = this.keys.dispatchKey(dispatchShardId);
15237
15848
  await this.visibilityManager.release(
15238
15849
  messageId,
15239
15850
  queueId,
15240
15851
  queueKey,
15241
15852
  queueItemsKey,
15242
- masterQueueKey,
15853
+ tenantQueueIndexKey,
15854
+ dispatchKey,
15855
+ descriptor.tenantId,
15243
15856
  Date.now()
15244
15857
  // Put at back of queue
15245
15858
  );
@@ -15263,7 +15876,6 @@ var FairQueue = class {
15263
15876
  const shardId = this.masterQueue.getShardForQueue(queueId);
15264
15877
  const queueKey = this.keys.queueKey(queueId);
15265
15878
  const queueItemsKey = this.keys.queueItemsKey(queueId);
15266
- const masterQueueKey = this.keys.masterQueueKey(shardId);
15267
15879
  const inflightDataKey = this.keys.inflightDataKey(shardId);
15268
15880
  const dataJson = await this.redis.hget(inflightDataKey, messageId);
15269
15881
  if (!dataJson) {
@@ -15285,12 +15897,13 @@ var FairQueue = class {
15285
15897
  tenantId: storedMessage.tenantId,
15286
15898
  metadata: storedMessage.metadata ?? {}
15287
15899
  };
15900
+ const dispatchShardId = this.tenantDispatch.getShardForTenant(descriptor.tenantId);
15288
15901
  await this.#handleMessageFailure(
15289
15902
  storedMessage,
15290
15903
  queueId,
15291
15904
  queueKey,
15292
15905
  queueItemsKey,
15293
- masterQueueKey,
15906
+ dispatchShardId,
15294
15907
  descriptor,
15295
15908
  error
15296
15909
  );
@@ -15298,7 +15911,7 @@ var FairQueue = class {
15298
15911
  // ============================================================================
15299
15912
  // Private - Message Processing Helpers
15300
15913
  // ============================================================================
15301
- async #handleMessageFailure(storedMessage, queueId, queueKey, queueItemsKey, masterQueueKey, descriptor, error) {
15914
+ async #handleMessageFailure(storedMessage, queueId, queueKey, queueItemsKey, dispatchShardId, descriptor, error) {
15302
15915
  this.telemetry.recordFailure();
15303
15916
  if (this.retryStrategy) {
15304
15917
  const nextDelay = this.retryStrategy.getNextDelay(storedMessage.attempt, error);
@@ -15307,15 +15920,19 @@ var FairQueue = class {
15307
15920
  ...storedMessage,
15308
15921
  attempt: storedMessage.attempt + 1
15309
15922
  };
15923
+ const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(descriptor.tenantId);
15924
+ const dispatchKey = this.keys.dispatchKey(dispatchShardId);
15310
15925
  await this.visibilityManager.release(
15311
15926
  storedMessage.id,
15312
15927
  queueId,
15313
15928
  queueKey,
15314
15929
  queueItemsKey,
15315
- masterQueueKey,
15316
- Date.now() + nextDelay
15930
+ tenantQueueIndexKey,
15931
+ dispatchKey,
15932
+ descriptor.tenantId,
15933
+ Date.now() + nextDelay,
15934
+ JSON.stringify(updatedMessage)
15317
15935
  );
15318
- await this.redis.hset(queueItemsKey, storedMessage.id, JSON.stringify(updatedMessage));
15319
15936
  if (this.concurrencyManager) {
15320
15937
  await this.concurrencyManager.release(descriptor, storedMessage.id);
15321
15938
  }
@@ -15393,28 +16010,36 @@ var FairQueue = class {
15393
16010
  async #reclaimTimedOutMessages() {
15394
16011
  let totalReclaimed = 0;
15395
16012
  for (let shardId = 0; shardId < this.shardCount; shardId++) {
15396
- const reclaimedMessages = await this.visibilityManager.reclaimTimedOut(shardId, (queueId) => ({
15397
- queueKey: this.keys.queueKey(queueId),
15398
- queueItemsKey: this.keys.queueItemsKey(queueId),
15399
- masterQueueKey: this.keys.masterQueueKey(this.masterQueue.getShardForQueue(queueId))
15400
- }));
15401
- if (this.concurrencyManager && reclaimedMessages.length > 0) {
15402
- try {
15403
- await this.concurrencyManager.releaseBatch(
15404
- reclaimedMessages.map((msg) => ({
15405
- queue: {
15406
- id: msg.queueId,
15407
- tenantId: msg.tenantId,
15408
- metadata: msg.metadata ?? {}
15409
- },
15410
- messageId: msg.messageId
15411
- }))
15412
- );
15413
- } catch (error) {
15414
- this.logger.error("Failed to release concurrency for reclaimed messages", {
15415
- count: reclaimedMessages.length,
15416
- error: error instanceof Error ? error.message : String(error)
15417
- });
16013
+ const reclaimedMessages = await this.visibilityManager.reclaimTimedOut(shardId, (queueId) => {
16014
+ const tenantId = this.keys.extractTenantId(queueId);
16015
+ const dispatchShardId = this.tenantDispatch.getShardForTenant(tenantId);
16016
+ return {
16017
+ queueKey: this.keys.queueKey(queueId),
16018
+ queueItemsKey: this.keys.queueItemsKey(queueId),
16019
+ tenantQueueIndexKey: this.keys.tenantQueueIndexKey(tenantId),
16020
+ dispatchKey: this.keys.dispatchKey(dispatchShardId),
16021
+ tenantId
16022
+ };
16023
+ });
16024
+ if (reclaimedMessages.length > 0) {
16025
+ if (this.concurrencyManager) {
16026
+ try {
16027
+ await this.concurrencyManager.releaseBatch(
16028
+ reclaimedMessages.map((msg) => ({
16029
+ queue: {
16030
+ id: msg.queueId,
16031
+ tenantId: msg.tenantId,
16032
+ metadata: msg.metadata ?? {}
16033
+ },
16034
+ messageId: msg.messageId
16035
+ }))
16036
+ );
16037
+ } catch (error) {
16038
+ this.logger.error("Failed to release concurrency for reclaimed messages", {
16039
+ count: reclaimedMessages.length,
16040
+ error: error instanceof Error ? error.message : String(error)
16041
+ });
16042
+ }
15418
16043
  }
15419
16044
  }
15420
16045
  totalReclaimed += reclaimedMessages.length;
@@ -15476,6 +16101,32 @@ var FairQueue = class {
15476
16101
  // ============================================================================
15477
16102
  // Private - Helpers
15478
16103
  // ============================================================================
16104
+ /**
16105
+ * Update both old master queue and new dispatch indexes after a dequeue/complete.
16106
+ * Both calls are idempotent - ZREM on a non-existent member is a no-op.
16107
+ * This handles the transition period where queues may exist in either or both indexes.
16108
+ */
16109
+ async #updateAllIndexesAfterDequeue(queueId, tenantId) {
16110
+ const queueShardId = this.masterQueue.getShardForQueue(queueId);
16111
+ const dispatchShardId = this.tenantDispatch.getShardForTenant(tenantId);
16112
+ const queueKey = this.keys.queueKey(queueId);
16113
+ const masterQueueKey = this.keys.masterQueueKey(queueShardId);
16114
+ const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(tenantId);
16115
+ const dispatchKey = this.keys.dispatchKey(dispatchShardId);
16116
+ const removedFromMaster = await this.redis.updateMasterQueueIfEmpty(
16117
+ masterQueueKey,
16118
+ queueKey,
16119
+ queueId
16120
+ );
16121
+ const removedFromDispatch = await this.redis.updateDispatchIndexes(
16122
+ queueKey,
16123
+ tenantQueueIndexKey,
16124
+ dispatchKey,
16125
+ queueId,
16126
+ tenantId
16127
+ );
16128
+ return { queueEmpty: removedFromMaster === 1 || removedFromDispatch === 1 };
16129
+ }
15479
16130
  #createSchedulerContext() {
15480
16131
  return {
15481
16132
  getCurrentConcurrency: async (groupName, groupId) => {
@@ -15515,13 +16166,9 @@ local messageId = ARGV[2]
15515
16166
  local timestamp = tonumber(ARGV[3])
15516
16167
  local payload = ARGV[4]
15517
16168
 
15518
- -- Add to sorted set (score = timestamp)
15519
16169
  redis.call('ZADD', queueKey, timestamp, messageId)
15520
-
15521
- -- Store payload in hash
15522
16170
  redis.call('HSET', queueItemsKey, messageId, payload)
15523
16171
 
15524
- -- Update master queue with oldest message timestamp
15525
16172
  local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
15526
16173
  if #oldest >= 2 then
15527
16174
  redis.call('ZADD', masterQueueKey, oldest[2], queueId)
@@ -15539,20 +16186,14 @@ local masterQueueKey = KEYS[3]
15539
16186
 
15540
16187
  local queueId = ARGV[1]
15541
16188
 
15542
- -- Args after queueId are triples: [messageId, timestamp, payload, ...]
15543
16189
  for i = 2, #ARGV, 3 do
15544
16190
  local messageId = ARGV[i]
15545
16191
  local timestamp = tonumber(ARGV[i + 1])
15546
16192
  local payload = ARGV[i + 2]
15547
-
15548
- -- Add to sorted set
15549
16193
  redis.call('ZADD', queueKey, timestamp, messageId)
15550
-
15551
- -- Store payload in hash
15552
16194
  redis.call('HSET', queueItemsKey, messageId, payload)
15553
16195
  end
15554
16196
 
15555
- -- Update master queue with oldest message timestamp
15556
16197
  local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
15557
16198
  if #oldest >= 2 then
15558
16199
  redis.call('ZADD', masterQueueKey, oldest[2], queueId)
@@ -15572,11 +16213,119 @@ local count = redis.call('ZCARD', queueKey)
15572
16213
  if count == 0 then
15573
16214
  redis.call('ZREM', masterQueueKey, queueId)
15574
16215
  return 1
16216
+ end
16217
+
16218
+ -- Queue still has messages but don't re-add to legacy master queue.
16219
+ -- New enqueues go through the V2 dispatch path, so we only drain here.
16220
+ -- Just remove it so it doesn't linger.
16221
+ redis.call('ZREM', masterQueueKey, queueId)
16222
+ return 0
16223
+ `
16224
+ });
16225
+ this.redis.defineCommand("enqueueMessageAtomicV2", {
16226
+ numberOfKeys: 4,
16227
+ lua: `
16228
+ local queueKey = KEYS[1]
16229
+ local queueItemsKey = KEYS[2]
16230
+ local tenantQueueIndexKey = KEYS[3]
16231
+ local dispatchKey = KEYS[4]
16232
+
16233
+ local queueId = ARGV[1]
16234
+ local messageId = ARGV[2]
16235
+ local timestamp = tonumber(ARGV[3])
16236
+ local payload = ARGV[4]
16237
+ local tenantId = ARGV[5]
16238
+
16239
+ -- Add to per-queue storage (same as before)
16240
+ redis.call('ZADD', queueKey, timestamp, messageId)
16241
+ redis.call('HSET', queueItemsKey, messageId, payload)
16242
+
16243
+ -- Update tenant queue index (Level 2) with queue's oldest message
16244
+ local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
16245
+ if #oldest >= 2 then
16246
+ redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
16247
+ end
16248
+
16249
+ -- Update dispatch index (Level 1) with tenant's oldest across all queues
16250
+ local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
16251
+ if #tenantOldest >= 2 then
16252
+ redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
16253
+ end
16254
+
16255
+ return 1
16256
+ `
16257
+ });
16258
+ this.redis.defineCommand("enqueueBatchAtomicV2", {
16259
+ numberOfKeys: 4,
16260
+ lua: `
16261
+ local queueKey = KEYS[1]
16262
+ local queueItemsKey = KEYS[2]
16263
+ local tenantQueueIndexKey = KEYS[3]
16264
+ local dispatchKey = KEYS[4]
16265
+
16266
+ local queueId = ARGV[1]
16267
+ local tenantId = ARGV[2]
16268
+
16269
+ -- Args after queueId and tenantId are triples: [messageId, timestamp, payload, ...]
16270
+ for i = 3, #ARGV, 3 do
16271
+ local messageId = ARGV[i]
16272
+ local timestamp = tonumber(ARGV[i + 1])
16273
+ local payload = ARGV[i + 2]
16274
+ redis.call('ZADD', queueKey, timestamp, messageId)
16275
+ redis.call('HSET', queueItemsKey, messageId, payload)
16276
+ end
16277
+
16278
+ -- Update tenant queue index (Level 2)
16279
+ local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
16280
+ if #oldest >= 2 then
16281
+ redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
16282
+ end
16283
+
16284
+ -- Update dispatch index (Level 1)
16285
+ local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
16286
+ if #tenantOldest >= 2 then
16287
+ redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
16288
+ end
16289
+
16290
+ return (#ARGV - 2) / 3
16291
+ `
16292
+ });
16293
+ this.redis.defineCommand("updateDispatchIndexes", {
16294
+ numberOfKeys: 3,
16295
+ lua: `
16296
+ local queueKey = KEYS[1]
16297
+ local tenantQueueIndexKey = KEYS[2]
16298
+ local dispatchKey = KEYS[3]
16299
+ local queueId = ARGV[1]
16300
+ local tenantId = ARGV[2]
16301
+
16302
+ local count = redis.call('ZCARD', queueKey)
16303
+ if count == 0 then
16304
+ -- Queue is empty: remove from tenant queue index
16305
+ redis.call('ZREM', tenantQueueIndexKey, queueId)
16306
+
16307
+ -- Check if tenant has any queues left
16308
+ local tenantQueueCount = redis.call('ZCARD', tenantQueueIndexKey)
16309
+ if tenantQueueCount == 0 then
16310
+ -- No more queues: remove tenant from dispatch
16311
+ redis.call('ZREM', dispatchKey, tenantId)
16312
+ else
16313
+ -- Update dispatch score to tenant's new oldest
16314
+ local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
16315
+ if #tenantOldest >= 2 then
16316
+ redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
16317
+ end
16318
+ end
16319
+ return 1
15575
16320
  else
15576
- -- Update with oldest message timestamp
16321
+ -- Queue still has messages: update scores
15577
16322
  local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
15578
16323
  if #oldest >= 2 then
15579
- redis.call('ZADD', masterQueueKey, oldest[2], queueId)
16324
+ redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
16325
+ end
16326
+ local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
16327
+ if #tenantOldest >= 2 then
16328
+ redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
15580
16329
  end
15581
16330
  return 0
15582
16331
  end
@@ -15588,6 +16337,6 @@ end
15588
16337
  }
15589
16338
  };
15590
16339
 
15591
- export { BaseScheduler, BatchedSpanManager, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, defaultRetryOptions, isAbortError, noopTelemetry };
16340
+ export { BaseScheduler, BatchedSpanManager, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, TenantDispatch, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, defaultRetryOptions, isAbortError, noopTelemetry };
15592
16341
  //# sourceMappingURL=index.js.map
15593
16342
  //# sourceMappingURL=index.js.map