@trigger.dev/redis-worker 4.4.1 → 4.4.3
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 +894 -144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -14
- package/dist/index.d.ts +195 -14
- package/dist/index.js +894 -145
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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',
|
|
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:
|
|
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
|
|
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) -
|
|
13214
|
-
local membersStart =
|
|
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
|
|
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',
|
|
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.
|
|
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
|
-
//
|
|
14511
|
-
|
|
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
|
|
14562
|
-
const
|
|
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.
|
|
15082
|
+
await this.redis.enqueueMessageAtomicV2(
|
|
14594
15083
|
queueKey,
|
|
14595
15084
|
queueItemsKey,
|
|
14596
|
-
|
|
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]:
|
|
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
|
|
14634
|
-
const
|
|
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.
|
|
15168
|
+
await this.redis.enqueueBatchAtomicV2(
|
|
14677
15169
|
queueKey,
|
|
14678
15170
|
queueItemsKey,
|
|
14679
|
-
|
|
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]:
|
|
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
|
|
15402
|
+
* Get total tenant count across dispatch shards plus any legacy queues still draining.
|
|
14908
15403
|
*/
|
|
14909
15404
|
async getTotalQueueCount() {
|
|
14910
|
-
|
|
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.#
|
|
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
|
-
|
|
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
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
"
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
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,
|
|
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.
|
|
15091
|
-
const
|
|
15092
|
-
if (
|
|
15093
|
-
|
|
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
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
15201
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
15316
|
-
|
|
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
|
-
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
|
|
15415
|
-
|
|
15416
|
-
|
|
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
|
-
--
|
|
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',
|
|
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
|