@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.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.cjs
CHANGED
|
@@ -11168,6 +11168,8 @@ var Worker = class _Worker {
|
|
|
11168
11168
|
shutdownTimeoutMs;
|
|
11169
11169
|
// The p-limit limiter to control overall concurrency.
|
|
11170
11170
|
limiters = {};
|
|
11171
|
+
// Batch accumulators: one per batch-enabled job type
|
|
11172
|
+
batchAccumulators = /* @__PURE__ */ new Map();
|
|
11171
11173
|
async #updateQueueSizeMetric(observableResult) {
|
|
11172
11174
|
const queueSize = await this.queue.size();
|
|
11173
11175
|
observableResult.observe(queueSize, {
|
|
@@ -11370,6 +11372,31 @@ var Worker = class _Worker {
|
|
|
11370
11372
|
async getJob(id) {
|
|
11371
11373
|
return this.queue.getJob(id);
|
|
11372
11374
|
}
|
|
11375
|
+
/**
|
|
11376
|
+
* Returns true if the given job type has batch config in the catalog.
|
|
11377
|
+
*/
|
|
11378
|
+
isBatchJob(jobType) {
|
|
11379
|
+
const catalogItem = this.options.catalog[jobType];
|
|
11380
|
+
return !!catalogItem?.batch;
|
|
11381
|
+
}
|
|
11382
|
+
/**
|
|
11383
|
+
* Returns the batch config for a job type, or undefined if not batch-enabled.
|
|
11384
|
+
*/
|
|
11385
|
+
getBatchConfig(jobType) {
|
|
11386
|
+
const catalogItem = this.options.catalog[jobType];
|
|
11387
|
+
return catalogItem?.batch;
|
|
11388
|
+
}
|
|
11389
|
+
/**
|
|
11390
|
+
* The max dequeue count: the largest batch maxSize across catalog entries,
|
|
11391
|
+
* falling back to tasksPerWorker for non-batch catalogs.
|
|
11392
|
+
*/
|
|
11393
|
+
get maxDequeueCount() {
|
|
11394
|
+
const batchSizes = Object.values(this.options.catalog).filter((entry) => !!entry.batch).map((entry) => entry.batch.maxSize);
|
|
11395
|
+
if (batchSizes.length > 0) {
|
|
11396
|
+
return Math.max(...batchSizes);
|
|
11397
|
+
}
|
|
11398
|
+
return this.concurrency.tasksPerWorker;
|
|
11399
|
+
}
|
|
11373
11400
|
/**
|
|
11374
11401
|
* The main loop that each worker runs. It repeatedly polls for items,
|
|
11375
11402
|
* processes them, and then waits before the next iteration.
|
|
@@ -11393,6 +11420,7 @@ var Worker = class _Worker {
|
|
|
11393
11420
|
concurrencyOptions: this.concurrency
|
|
11394
11421
|
});
|
|
11395
11422
|
while (!this.isShuttingDown) {
|
|
11423
|
+
await this.flushTimedOutBatches(workerId, limiter);
|
|
11396
11424
|
if (limiter.activeCount + limiter.pendingCount >= this.concurrency.limit) {
|
|
11397
11425
|
this.logger.debug("Worker at capacity, waiting", {
|
|
11398
11426
|
workerId,
|
|
@@ -11404,9 +11432,10 @@ var Worker = class _Worker {
|
|
|
11404
11432
|
continue;
|
|
11405
11433
|
}
|
|
11406
11434
|
const $taskCount = Math.min(
|
|
11407
|
-
|
|
11435
|
+
this.maxDequeueCount,
|
|
11408
11436
|
this.concurrency.limit - limiter.activeCount - limiter.pendingCount
|
|
11409
11437
|
);
|
|
11438
|
+
let itemsFound = false;
|
|
11410
11439
|
try {
|
|
11411
11440
|
const items = await this.withHistogram(
|
|
11412
11441
|
this.metrics.dequeueDuration,
|
|
@@ -11416,7 +11445,8 @@ var Worker = class _Worker {
|
|
|
11416
11445
|
task_count: $taskCount
|
|
11417
11446
|
}
|
|
11418
11447
|
);
|
|
11419
|
-
|
|
11448
|
+
itemsFound = items.length > 0;
|
|
11449
|
+
if (items.length === 0 && this.batchAccumulators.size === 0) {
|
|
11420
11450
|
this.logger.debug("No items to dequeue", {
|
|
11421
11451
|
workerId,
|
|
11422
11452
|
concurrencyOptions: this.concurrency,
|
|
@@ -11426,29 +11456,226 @@ var Worker = class _Worker {
|
|
|
11426
11456
|
await _Worker.delay(pollIntervalMs);
|
|
11427
11457
|
continue;
|
|
11428
11458
|
}
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
for (const item of items) {
|
|
11437
|
-
limiter(
|
|
11438
|
-
() => this.processItem(item, items.length, workerId, limiter)
|
|
11439
|
-
).catch((err) => {
|
|
11440
|
-
this.logger.error("Unhandled error in processItem:", { error: err, workerId, item });
|
|
11459
|
+
if (items.length > 0) {
|
|
11460
|
+
this.logger.debug("Dequeued items", {
|
|
11461
|
+
workerId,
|
|
11462
|
+
itemCount: items.length,
|
|
11463
|
+
concurrencyOptions: this.concurrency,
|
|
11464
|
+
activeCount: limiter.activeCount,
|
|
11465
|
+
pendingCount: limiter.pendingCount
|
|
11441
11466
|
});
|
|
11442
11467
|
}
|
|
11468
|
+
for (const item of items) {
|
|
11469
|
+
const queueItem = item;
|
|
11470
|
+
if (this.isBatchJob(queueItem.job)) {
|
|
11471
|
+
this.addToAccumulator(queueItem);
|
|
11472
|
+
const batchConfig = this.getBatchConfig(queueItem.job);
|
|
11473
|
+
const accumulator = this.batchAccumulators.get(queueItem.job);
|
|
11474
|
+
if (accumulator && accumulator.items.length >= batchConfig.maxSize) {
|
|
11475
|
+
await this.flushBatch(queueItem.job, workerId, limiter);
|
|
11476
|
+
}
|
|
11477
|
+
} else {
|
|
11478
|
+
limiter(
|
|
11479
|
+
() => this.processItem(queueItem, items.length, workerId, limiter)
|
|
11480
|
+
).catch((err) => {
|
|
11481
|
+
this.logger.error("Unhandled error in processItem:", {
|
|
11482
|
+
error: err,
|
|
11483
|
+
workerId,
|
|
11484
|
+
item
|
|
11485
|
+
});
|
|
11486
|
+
});
|
|
11487
|
+
}
|
|
11488
|
+
}
|
|
11443
11489
|
} catch (error) {
|
|
11444
11490
|
this.logger.error("Error dequeuing items:", { name: this.options.name, error });
|
|
11445
11491
|
await _Worker.delay(pollIntervalMs);
|
|
11446
11492
|
continue;
|
|
11447
11493
|
}
|
|
11448
|
-
|
|
11494
|
+
if (itemsFound || this.batchAccumulators.size > 0) {
|
|
11495
|
+
await _Worker.delay(immediatePollIntervalMs);
|
|
11496
|
+
} else {
|
|
11497
|
+
await _Worker.delay(pollIntervalMs);
|
|
11498
|
+
}
|
|
11449
11499
|
}
|
|
11500
|
+
await this.flushAllBatches(workerId, limiter);
|
|
11450
11501
|
this.logger.info("Worker loop finished", { workerId });
|
|
11451
11502
|
}
|
|
11503
|
+
/**
|
|
11504
|
+
* Adds an item to the batch accumulator for its job type.
|
|
11505
|
+
*/
|
|
11506
|
+
addToAccumulator(item) {
|
|
11507
|
+
let accumulator = this.batchAccumulators.get(item.job);
|
|
11508
|
+
if (!accumulator) {
|
|
11509
|
+
accumulator = { items: [], firstItemAt: Date.now() };
|
|
11510
|
+
this.batchAccumulators.set(item.job, accumulator);
|
|
11511
|
+
}
|
|
11512
|
+
accumulator.items.push(item);
|
|
11513
|
+
}
|
|
11514
|
+
/**
|
|
11515
|
+
* Flushes any batch accumulators that have exceeded their maxWaitMs.
|
|
11516
|
+
*/
|
|
11517
|
+
async flushTimedOutBatches(workerId, limiter) {
|
|
11518
|
+
const now = Date.now();
|
|
11519
|
+
for (const [jobType, accumulator] of this.batchAccumulators) {
|
|
11520
|
+
const batchConfig = this.getBatchConfig(jobType);
|
|
11521
|
+
if (!batchConfig) continue;
|
|
11522
|
+
if (now - accumulator.firstItemAt >= batchConfig.maxWaitMs) {
|
|
11523
|
+
await this.flushBatch(jobType, workerId, limiter);
|
|
11524
|
+
}
|
|
11525
|
+
}
|
|
11526
|
+
}
|
|
11527
|
+
/**
|
|
11528
|
+
* Flushes all batch accumulators (used during shutdown).
|
|
11529
|
+
*/
|
|
11530
|
+
async flushAllBatches(workerId, limiter) {
|
|
11531
|
+
for (const jobType of this.batchAccumulators.keys()) {
|
|
11532
|
+
await this.flushBatch(jobType, workerId, limiter);
|
|
11533
|
+
}
|
|
11534
|
+
}
|
|
11535
|
+
/**
|
|
11536
|
+
* Flushes the batch accumulator for a specific job type.
|
|
11537
|
+
* Removes items from the accumulator and submits them to the limiter as a single batch.
|
|
11538
|
+
*/
|
|
11539
|
+
async flushBatch(jobType, workerId, limiter) {
|
|
11540
|
+
const accumulator = this.batchAccumulators.get(jobType);
|
|
11541
|
+
if (!accumulator || accumulator.items.length === 0) return;
|
|
11542
|
+
const items = accumulator.items;
|
|
11543
|
+
this.batchAccumulators.delete(jobType);
|
|
11544
|
+
this.logger.debug("Flushing batch", {
|
|
11545
|
+
jobType,
|
|
11546
|
+
batchSize: items.length,
|
|
11547
|
+
workerId,
|
|
11548
|
+
accumulatedMs: Date.now() - accumulator.firstItemAt
|
|
11549
|
+
});
|
|
11550
|
+
limiter(() => this.processBatch(items, jobType, workerId, limiter)).catch((err) => {
|
|
11551
|
+
this.logger.error("Unhandled error in processBatch:", {
|
|
11552
|
+
error: err,
|
|
11553
|
+
workerId,
|
|
11554
|
+
jobType,
|
|
11555
|
+
batchSize: items.length
|
|
11556
|
+
});
|
|
11557
|
+
});
|
|
11558
|
+
}
|
|
11559
|
+
/**
|
|
11560
|
+
* Processes a batch of items for a batch-enabled job type.
|
|
11561
|
+
*/
|
|
11562
|
+
async processBatch(items, jobType, workerId, limiter) {
|
|
11563
|
+
const catalogItem = this.options.catalog[jobType];
|
|
11564
|
+
const handler = this.jobs[jobType];
|
|
11565
|
+
if (!handler) {
|
|
11566
|
+
this.logger.error(`Worker no handler found for batch job type: ${jobType}`);
|
|
11567
|
+
return;
|
|
11568
|
+
}
|
|
11569
|
+
if (!catalogItem) {
|
|
11570
|
+
this.logger.error(`Worker no catalog item found for batch job type: ${jobType}`);
|
|
11571
|
+
return;
|
|
11572
|
+
}
|
|
11573
|
+
const batchParams = items.map((item) => ({
|
|
11574
|
+
id: item.id,
|
|
11575
|
+
payload: item.item,
|
|
11576
|
+
visibilityTimeoutMs: item.visibilityTimeoutMs,
|
|
11577
|
+
attempt: item.attempt,
|
|
11578
|
+
deduplicationKey: item.deduplicationKey
|
|
11579
|
+
}));
|
|
11580
|
+
await startSpan(
|
|
11581
|
+
this.tracer,
|
|
11582
|
+
"processBatch",
|
|
11583
|
+
async () => {
|
|
11584
|
+
await this.withHistogram(this.metrics.jobDuration, handler(batchParams), {
|
|
11585
|
+
worker_id: workerId,
|
|
11586
|
+
batch_size: items.length,
|
|
11587
|
+
job_type: jobType
|
|
11588
|
+
});
|
|
11589
|
+
await Promise.all(items.map((item) => this.queue.ack(item.id, item.deduplicationKey)));
|
|
11590
|
+
},
|
|
11591
|
+
{
|
|
11592
|
+
kind: SpanKind.CONSUMER,
|
|
11593
|
+
attributes: {
|
|
11594
|
+
job_type: jobType,
|
|
11595
|
+
batch_size: items.length,
|
|
11596
|
+
worker_id: workerId,
|
|
11597
|
+
worker_name: this.options.name
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
).catch(async (error) => {
|
|
11601
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
11602
|
+
const shouldLogError = catalogItem.logErrors ?? true;
|
|
11603
|
+
if (shouldLogError) {
|
|
11604
|
+
this.logger.error(`Worker error processing batch`, {
|
|
11605
|
+
name: this.options.name,
|
|
11606
|
+
jobType,
|
|
11607
|
+
batchSize: items.length,
|
|
11608
|
+
error,
|
|
11609
|
+
errorMessage
|
|
11610
|
+
});
|
|
11611
|
+
} else {
|
|
11612
|
+
this.logger.info(`Worker failed to process batch`, {
|
|
11613
|
+
name: this.options.name,
|
|
11614
|
+
jobType,
|
|
11615
|
+
batchSize: items.length,
|
|
11616
|
+
error,
|
|
11617
|
+
errorMessage
|
|
11618
|
+
});
|
|
11619
|
+
}
|
|
11620
|
+
for (const item of items) {
|
|
11621
|
+
try {
|
|
11622
|
+
const newAttempt = item.attempt + 1;
|
|
11623
|
+
const retrySettings = {
|
|
11624
|
+
...defaultRetrySettings,
|
|
11625
|
+
...catalogItem?.retry
|
|
11626
|
+
};
|
|
11627
|
+
const retryDelay = v3.calculateNextRetryDelay(retrySettings, newAttempt);
|
|
11628
|
+
if (!retryDelay) {
|
|
11629
|
+
if (shouldLogError) {
|
|
11630
|
+
this.logger.error(`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
|
+
} else {
|
|
11637
|
+
this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, {
|
|
11638
|
+
name: this.options.name,
|
|
11639
|
+
id: item.id,
|
|
11640
|
+
jobType,
|
|
11641
|
+
attempt: newAttempt
|
|
11642
|
+
});
|
|
11643
|
+
}
|
|
11644
|
+
await this.queue.moveToDeadLetterQueue(item.id, errorMessage);
|
|
11645
|
+
continue;
|
|
11646
|
+
}
|
|
11647
|
+
const retryDate = new Date(Date.now() + retryDelay);
|
|
11648
|
+
this.logger.info(`Worker requeuing failed batch item with delay`, {
|
|
11649
|
+
name: this.options.name,
|
|
11650
|
+
id: item.id,
|
|
11651
|
+
jobType,
|
|
11652
|
+
retryDate,
|
|
11653
|
+
retryDelay,
|
|
11654
|
+
attempt: newAttempt
|
|
11655
|
+
});
|
|
11656
|
+
await this.queue.enqueue({
|
|
11657
|
+
id: item.id,
|
|
11658
|
+
job: item.job,
|
|
11659
|
+
item: item.item,
|
|
11660
|
+
availableAt: retryDate,
|
|
11661
|
+
attempt: newAttempt,
|
|
11662
|
+
visibilityTimeoutMs: item.visibilityTimeoutMs
|
|
11663
|
+
});
|
|
11664
|
+
} catch (requeueError) {
|
|
11665
|
+
this.logger.error(
|
|
11666
|
+
`Worker failed to requeue batch item. It will be retried after the visibility timeout.`,
|
|
11667
|
+
{
|
|
11668
|
+
name: this.options.name,
|
|
11669
|
+
id: item.id,
|
|
11670
|
+
jobType,
|
|
11671
|
+
visibilityTimeoutMs: item.visibilityTimeoutMs,
|
|
11672
|
+
error: requeueError
|
|
11673
|
+
}
|
|
11674
|
+
);
|
|
11675
|
+
}
|
|
11676
|
+
}
|
|
11677
|
+
});
|
|
11678
|
+
}
|
|
11452
11679
|
/**
|
|
11453
11680
|
* Processes a single item.
|
|
11454
11681
|
*/
|
|
@@ -12159,6 +12386,135 @@ return 1
|
|
|
12159
12386
|
});
|
|
12160
12387
|
}
|
|
12161
12388
|
};
|
|
12389
|
+
var TenantDispatch = class {
|
|
12390
|
+
constructor(options) {
|
|
12391
|
+
this.options = options;
|
|
12392
|
+
this.redis = createRedisClient(options.redis);
|
|
12393
|
+
this.keys = options.keys;
|
|
12394
|
+
this.shardCount = Math.max(1, options.shardCount);
|
|
12395
|
+
}
|
|
12396
|
+
redis;
|
|
12397
|
+
keys;
|
|
12398
|
+
shardCount;
|
|
12399
|
+
/**
|
|
12400
|
+
* Get the dispatch shard ID for a tenant.
|
|
12401
|
+
* Uses jump consistent hash on the tenant ID so each tenant
|
|
12402
|
+
* always maps to exactly one dispatch shard.
|
|
12403
|
+
*/
|
|
12404
|
+
getShardForTenant(tenantId) {
|
|
12405
|
+
return serverOnly.jumpHash(tenantId, this.shardCount);
|
|
12406
|
+
}
|
|
12407
|
+
/**
|
|
12408
|
+
* Get eligible tenants from a dispatch shard (Level 1).
|
|
12409
|
+
* Returns tenants ordered by oldest message (lowest score first).
|
|
12410
|
+
*/
|
|
12411
|
+
async getTenantsFromShard(shardId, limit = 1e3, maxScore) {
|
|
12412
|
+
const dispatchKey = this.keys.dispatchKey(shardId);
|
|
12413
|
+
const score = maxScore ?? Date.now();
|
|
12414
|
+
const results = await this.redis.zrangebyscore(
|
|
12415
|
+
dispatchKey,
|
|
12416
|
+
"-inf",
|
|
12417
|
+
score,
|
|
12418
|
+
"WITHSCORES",
|
|
12419
|
+
"LIMIT",
|
|
12420
|
+
0,
|
|
12421
|
+
limit
|
|
12422
|
+
);
|
|
12423
|
+
const tenants = [];
|
|
12424
|
+
for (let i = 0; i < results.length; i += 2) {
|
|
12425
|
+
const tenantId = results[i];
|
|
12426
|
+
const scoreStr = results[i + 1];
|
|
12427
|
+
if (tenantId && scoreStr) {
|
|
12428
|
+
tenants.push({
|
|
12429
|
+
tenantId,
|
|
12430
|
+
score: parseFloat(scoreStr)
|
|
12431
|
+
});
|
|
12432
|
+
}
|
|
12433
|
+
}
|
|
12434
|
+
return tenants;
|
|
12435
|
+
}
|
|
12436
|
+
/**
|
|
12437
|
+
* Get queues for a specific tenant (Level 2).
|
|
12438
|
+
* Returns queues ordered by oldest message (lowest score first).
|
|
12439
|
+
*/
|
|
12440
|
+
async getQueuesForTenant(tenantId, limit = 1e3, maxScore) {
|
|
12441
|
+
const tenantQueueKey = this.keys.tenantQueueIndexKey(tenantId);
|
|
12442
|
+
const score = maxScore ?? Date.now();
|
|
12443
|
+
const results = await this.redis.zrangebyscore(
|
|
12444
|
+
tenantQueueKey,
|
|
12445
|
+
"-inf",
|
|
12446
|
+
score,
|
|
12447
|
+
"WITHSCORES",
|
|
12448
|
+
"LIMIT",
|
|
12449
|
+
0,
|
|
12450
|
+
limit
|
|
12451
|
+
);
|
|
12452
|
+
const queues = [];
|
|
12453
|
+
for (let i = 0; i < results.length; i += 2) {
|
|
12454
|
+
const queueId = results[i];
|
|
12455
|
+
const scoreStr = results[i + 1];
|
|
12456
|
+
if (queueId && scoreStr) {
|
|
12457
|
+
queues.push({
|
|
12458
|
+
queueId,
|
|
12459
|
+
score: parseFloat(scoreStr),
|
|
12460
|
+
tenantId
|
|
12461
|
+
});
|
|
12462
|
+
}
|
|
12463
|
+
}
|
|
12464
|
+
return queues;
|
|
12465
|
+
}
|
|
12466
|
+
/**
|
|
12467
|
+
* Get the number of tenants in a dispatch shard.
|
|
12468
|
+
*/
|
|
12469
|
+
async getShardTenantCount(shardId) {
|
|
12470
|
+
const dispatchKey = this.keys.dispatchKey(shardId);
|
|
12471
|
+
return await this.redis.zcard(dispatchKey);
|
|
12472
|
+
}
|
|
12473
|
+
/**
|
|
12474
|
+
* Get total tenant count across all dispatch shards.
|
|
12475
|
+
* Note: tenants may appear in multiple shards, so this may overcount.
|
|
12476
|
+
*/
|
|
12477
|
+
async getTotalTenantCount() {
|
|
12478
|
+
const counts = await Promise.all(
|
|
12479
|
+
Array.from({ length: this.shardCount }, (_, i) => this.getShardTenantCount(i))
|
|
12480
|
+
);
|
|
12481
|
+
return counts.reduce((sum, count) => sum + count, 0);
|
|
12482
|
+
}
|
|
12483
|
+
/**
|
|
12484
|
+
* Get the number of queues for a tenant.
|
|
12485
|
+
*/
|
|
12486
|
+
async getTenantQueueCount(tenantId) {
|
|
12487
|
+
const tenantQueueKey = this.keys.tenantQueueIndexKey(tenantId);
|
|
12488
|
+
return await this.redis.zcard(tenantQueueKey);
|
|
12489
|
+
}
|
|
12490
|
+
/**
|
|
12491
|
+
* Remove a tenant from a specific dispatch shard.
|
|
12492
|
+
*/
|
|
12493
|
+
async removeTenantFromShard(shardId, tenantId) {
|
|
12494
|
+
const dispatchKey = this.keys.dispatchKey(shardId);
|
|
12495
|
+
await this.redis.zrem(dispatchKey, tenantId);
|
|
12496
|
+
}
|
|
12497
|
+
/**
|
|
12498
|
+
* Add a tenant to a dispatch shard with the given score.
|
|
12499
|
+
*/
|
|
12500
|
+
async addTenantToShard(shardId, tenantId, score) {
|
|
12501
|
+
const dispatchKey = this.keys.dispatchKey(shardId);
|
|
12502
|
+
await this.redis.zadd(dispatchKey, score, tenantId);
|
|
12503
|
+
}
|
|
12504
|
+
/**
|
|
12505
|
+
* Remove a queue from a tenant's queue index.
|
|
12506
|
+
*/
|
|
12507
|
+
async removeQueueFromTenant(tenantId, queueId) {
|
|
12508
|
+
const tenantQueueKey = this.keys.tenantQueueIndexKey(tenantId);
|
|
12509
|
+
await this.redis.zrem(tenantQueueKey, queueId);
|
|
12510
|
+
}
|
|
12511
|
+
/**
|
|
12512
|
+
* Close the Redis connection.
|
|
12513
|
+
*/
|
|
12514
|
+
async close() {
|
|
12515
|
+
await this.redis.quit();
|
|
12516
|
+
}
|
|
12517
|
+
};
|
|
12162
12518
|
|
|
12163
12519
|
// src/fair-queue/telemetry.ts
|
|
12164
12520
|
var FairQueueAttributes = {
|
|
@@ -12340,6 +12696,18 @@ var FairQueueTelemetry = class {
|
|
|
12340
12696
|
}
|
|
12341
12697
|
});
|
|
12342
12698
|
}
|
|
12699
|
+
if (callbacks.getDispatchLength && callbacks.shardCount) {
|
|
12700
|
+
const getDispatchLength = callbacks.getDispatchLength;
|
|
12701
|
+
const shardCount = callbacks.shardCount;
|
|
12702
|
+
this.metrics.dispatchLength.addCallback(async (observableResult) => {
|
|
12703
|
+
for (let shardId = 0; shardId < shardCount; shardId++) {
|
|
12704
|
+
const length = await getDispatchLength(shardId);
|
|
12705
|
+
observableResult.observe(length, {
|
|
12706
|
+
[FairQueueAttributes.SHARD_ID]: shardId.toString()
|
|
12707
|
+
});
|
|
12708
|
+
}
|
|
12709
|
+
});
|
|
12710
|
+
}
|
|
12343
12711
|
if (callbacks.getInflightCount && callbacks.shardCount) {
|
|
12344
12712
|
const getInflightCount = callbacks.getInflightCount;
|
|
12345
12713
|
const shardCount = callbacks.shardCount;
|
|
@@ -12442,9 +12810,13 @@ var FairQueueTelemetry = class {
|
|
|
12442
12810
|
unit: "messages"
|
|
12443
12811
|
}),
|
|
12444
12812
|
masterQueueLength: this.meter.createObservableGauge(`${this.name}.master_queue.length`, {
|
|
12445
|
-
description: "Number of queues in master queue shard",
|
|
12813
|
+
description: "Number of queues in legacy master queue shard (draining)",
|
|
12446
12814
|
unit: "queues"
|
|
12447
12815
|
}),
|
|
12816
|
+
dispatchLength: this.meter.createObservableGauge(`${this.name}.dispatch.length`, {
|
|
12817
|
+
description: "Number of tenants in dispatch index shard",
|
|
12818
|
+
unit: "tenants"
|
|
12819
|
+
}),
|
|
12448
12820
|
inflightCount: this.meter.createObservableGauge(`${this.name}.inflight.count`, {
|
|
12449
12821
|
description: "Number of messages currently being processed",
|
|
12450
12822
|
unit: "messages"
|
|
@@ -12848,10 +13220,12 @@ var VisibilityManager = class {
|
|
|
12848
13220
|
* @param queueId - The queue ID
|
|
12849
13221
|
* @param queueKey - The Redis key for the queue
|
|
12850
13222
|
* @param queueItemsKey - The Redis key for the queue items hash
|
|
12851
|
-
* @param
|
|
13223
|
+
* @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
|
|
13224
|
+
* @param dispatchKey - The Redis key for the dispatch index (Level 1)
|
|
13225
|
+
* @param tenantId - The tenant ID
|
|
12852
13226
|
* @param score - Optional score for the message (defaults to now)
|
|
12853
13227
|
*/
|
|
12854
|
-
async release(messageId, queueId, queueKey, queueItemsKey,
|
|
13228
|
+
async release(messageId, queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, tenantId, score, updatedData) {
|
|
12855
13229
|
const shardId = this.#getShardForQueue(queueId);
|
|
12856
13230
|
const inflightKey = this.keys.inflightKey(shardId);
|
|
12857
13231
|
const inflightDataKey = this.keys.inflightDataKey(shardId);
|
|
@@ -12862,11 +13236,14 @@ var VisibilityManager = class {
|
|
|
12862
13236
|
inflightDataKey,
|
|
12863
13237
|
queueKey,
|
|
12864
13238
|
queueItemsKey,
|
|
12865
|
-
|
|
13239
|
+
tenantQueueIndexKey,
|
|
13240
|
+
dispatchKey,
|
|
12866
13241
|
member,
|
|
12867
13242
|
messageId,
|
|
12868
13243
|
messageScore.toString(),
|
|
12869
|
-
queueId
|
|
13244
|
+
queueId,
|
|
13245
|
+
updatedData ?? "",
|
|
13246
|
+
tenantId
|
|
12870
13247
|
);
|
|
12871
13248
|
this.logger.debug("Message released", {
|
|
12872
13249
|
messageId,
|
|
@@ -12883,10 +13260,12 @@ var VisibilityManager = class {
|
|
|
12883
13260
|
* @param queueId - The queue ID
|
|
12884
13261
|
* @param queueKey - The Redis key for the queue
|
|
12885
13262
|
* @param queueItemsKey - The Redis key for the queue items hash
|
|
12886
|
-
* @param
|
|
13263
|
+
* @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
|
|
13264
|
+
* @param dispatchKey - The Redis key for the dispatch index (Level 1)
|
|
13265
|
+
* @param tenantId - The tenant ID
|
|
12887
13266
|
* @param score - Optional score for the messages (defaults to now)
|
|
12888
13267
|
*/
|
|
12889
|
-
async releaseBatch(messages, queueId, queueKey, queueItemsKey,
|
|
13268
|
+
async releaseBatch(messages, queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, tenantId, score) {
|
|
12890
13269
|
if (messages.length === 0) {
|
|
12891
13270
|
return;
|
|
12892
13271
|
}
|
|
@@ -12901,9 +13280,11 @@ var VisibilityManager = class {
|
|
|
12901
13280
|
inflightDataKey,
|
|
12902
13281
|
queueKey,
|
|
12903
13282
|
queueItemsKey,
|
|
12904
|
-
|
|
13283
|
+
tenantQueueIndexKey,
|
|
13284
|
+
dispatchKey,
|
|
12905
13285
|
messageScore.toString(),
|
|
12906
13286
|
queueId,
|
|
13287
|
+
tenantId,
|
|
12907
13288
|
...members,
|
|
12908
13289
|
...messageIds
|
|
12909
13290
|
);
|
|
@@ -12943,7 +13324,7 @@ var VisibilityManager = class {
|
|
|
12943
13324
|
continue;
|
|
12944
13325
|
}
|
|
12945
13326
|
const { messageId, queueId } = this.#parseMember(member);
|
|
12946
|
-
const { queueKey, queueItemsKey,
|
|
13327
|
+
const { queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, tenantId } = getQueueKeys(queueId);
|
|
12947
13328
|
try {
|
|
12948
13329
|
const dataJson = await this.redis.hget(inflightDataKey, messageId);
|
|
12949
13330
|
let storedMessage = null;
|
|
@@ -12959,11 +13340,14 @@ var VisibilityManager = class {
|
|
|
12959
13340
|
inflightDataKey,
|
|
12960
13341
|
queueKey,
|
|
12961
13342
|
queueItemsKey,
|
|
12962
|
-
|
|
13343
|
+
tenantQueueIndexKey,
|
|
13344
|
+
dispatchKey,
|
|
12963
13345
|
member,
|
|
12964
13346
|
messageId,
|
|
12965
13347
|
score.toString(),
|
|
12966
|
-
queueId
|
|
13348
|
+
queueId,
|
|
13349
|
+
"",
|
|
13350
|
+
tenantId
|
|
12967
13351
|
);
|
|
12968
13352
|
if (storedMessage) {
|
|
12969
13353
|
reclaimedMessages.push({
|
|
@@ -13164,18 +13548,21 @@ return results
|
|
|
13164
13548
|
`
|
|
13165
13549
|
});
|
|
13166
13550
|
this.redis.defineCommand("releaseMessage", {
|
|
13167
|
-
numberOfKeys:
|
|
13551
|
+
numberOfKeys: 6,
|
|
13168
13552
|
lua: `
|
|
13169
13553
|
local inflightKey = KEYS[1]
|
|
13170
13554
|
local inflightDataKey = KEYS[2]
|
|
13171
13555
|
local queueKey = KEYS[3]
|
|
13172
13556
|
local queueItemsKey = KEYS[4]
|
|
13173
|
-
local
|
|
13557
|
+
local tenantQueueIndexKey = KEYS[5]
|
|
13558
|
+
local dispatchKey = KEYS[6]
|
|
13174
13559
|
|
|
13175
13560
|
local member = ARGV[1]
|
|
13176
13561
|
local messageId = ARGV[2]
|
|
13177
13562
|
local score = tonumber(ARGV[3])
|
|
13178
13563
|
local queueId = ARGV[4]
|
|
13564
|
+
local updatedData = ARGV[5]
|
|
13565
|
+
local tenantId = ARGV[6]
|
|
13179
13566
|
|
|
13180
13567
|
-- Get message data from in-flight
|
|
13181
13568
|
local payload = redis.call('HGET', inflightDataKey, messageId)
|
|
@@ -13184,6 +13571,12 @@ if not payload then
|
|
|
13184
13571
|
return 0
|
|
13185
13572
|
end
|
|
13186
13573
|
|
|
13574
|
+
-- Use updatedData if provided (e.g. incremented attempt count for retries),
|
|
13575
|
+
-- otherwise use the original in-flight data
|
|
13576
|
+
if updatedData and updatedData ~= "" then
|
|
13577
|
+
payload = updatedData
|
|
13578
|
+
end
|
|
13579
|
+
|
|
13187
13580
|
-- Remove from in-flight
|
|
13188
13581
|
redis.call('ZREM', inflightKey, member)
|
|
13189
13582
|
redis.call('HDEL', inflightDataKey, messageId)
|
|
@@ -13192,33 +13585,39 @@ redis.call('HDEL', inflightDataKey, messageId)
|
|
|
13192
13585
|
redis.call('ZADD', queueKey, score, messageId)
|
|
13193
13586
|
redis.call('HSET', queueItemsKey, messageId, payload)
|
|
13194
13587
|
|
|
13195
|
-
-- Update
|
|
13196
|
-
-- This ensures delayed messages don't push the queue priority to the future
|
|
13197
|
-
-- when there are other ready messages in the queue
|
|
13588
|
+
-- Update tenant queue index (Level 2) with queue's oldest message
|
|
13198
13589
|
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
13199
13590
|
if #oldest >= 2 then
|
|
13200
|
-
redis.call('ZADD',
|
|
13591
|
+
redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
|
|
13592
|
+
end
|
|
13593
|
+
|
|
13594
|
+
-- Update dispatch index (Level 1) with tenant's oldest across all queues
|
|
13595
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
13596
|
+
if #tenantOldest >= 2 then
|
|
13597
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
13201
13598
|
end
|
|
13202
13599
|
|
|
13203
13600
|
return 1
|
|
13204
13601
|
`
|
|
13205
13602
|
});
|
|
13206
13603
|
this.redis.defineCommand("releaseMessageBatch", {
|
|
13207
|
-
numberOfKeys:
|
|
13604
|
+
numberOfKeys: 6,
|
|
13208
13605
|
lua: `
|
|
13209
13606
|
local inflightKey = KEYS[1]
|
|
13210
13607
|
local inflightDataKey = KEYS[2]
|
|
13211
13608
|
local queueKey = KEYS[3]
|
|
13212
13609
|
local queueItemsKey = KEYS[4]
|
|
13213
|
-
local
|
|
13610
|
+
local tenantQueueIndexKey = KEYS[5]
|
|
13611
|
+
local dispatchKey = KEYS[6]
|
|
13214
13612
|
|
|
13215
13613
|
local score = tonumber(ARGV[1])
|
|
13216
13614
|
local queueId = ARGV[2]
|
|
13615
|
+
local tenantId = ARGV[3]
|
|
13217
13616
|
|
|
13218
13617
|
-- Remaining args are: members..., messageIds...
|
|
13219
13618
|
-- Calculate how many messages we have
|
|
13220
|
-
local numMessages = (table.getn(ARGV) -
|
|
13221
|
-
local membersStart =
|
|
13619
|
+
local numMessages = (table.getn(ARGV) - 3) / 2
|
|
13620
|
+
local membersStart = 4
|
|
13222
13621
|
local messageIdsStart = membersStart + numMessages
|
|
13223
13622
|
|
|
13224
13623
|
local releasedCount = 0
|
|
@@ -13226,27 +13625,33 @@ local releasedCount = 0
|
|
|
13226
13625
|
for i = 0, numMessages - 1 do
|
|
13227
13626
|
local member = ARGV[membersStart + i]
|
|
13228
13627
|
local messageId = ARGV[messageIdsStart + i]
|
|
13229
|
-
|
|
13628
|
+
|
|
13230
13629
|
-- Get message data from in-flight
|
|
13231
13630
|
local payload = redis.call('HGET', inflightDataKey, messageId)
|
|
13232
13631
|
if payload then
|
|
13233
13632
|
-- Remove from in-flight
|
|
13234
13633
|
redis.call('ZREM', inflightKey, member)
|
|
13235
13634
|
redis.call('HDEL', inflightDataKey, messageId)
|
|
13236
|
-
|
|
13635
|
+
|
|
13237
13636
|
-- Add back to queue
|
|
13238
13637
|
redis.call('ZADD', queueKey, score, messageId)
|
|
13239
13638
|
redis.call('HSET', queueItemsKey, messageId, payload)
|
|
13240
|
-
|
|
13639
|
+
|
|
13241
13640
|
releasedCount = releasedCount + 1
|
|
13242
13641
|
end
|
|
13243
13642
|
end
|
|
13244
13643
|
|
|
13245
|
-
-- Update
|
|
13644
|
+
-- Update dispatch indexes (only once at the end)
|
|
13246
13645
|
if releasedCount > 0 then
|
|
13646
|
+
-- Update tenant queue index (Level 2)
|
|
13247
13647
|
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
13248
13648
|
if #oldest >= 2 then
|
|
13249
|
-
redis.call('ZADD',
|
|
13649
|
+
redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
|
|
13650
|
+
end
|
|
13651
|
+
-- Update dispatch index (Level 1)
|
|
13652
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
13653
|
+
if #tenantOldest >= 2 then
|
|
13654
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
13250
13655
|
end
|
|
13251
13656
|
end
|
|
13252
13657
|
|
|
@@ -13534,6 +13939,15 @@ var DefaultFairQueueKeyProducer = class {
|
|
|
13534
13939
|
return this.#buildKey("worker", consumerId);
|
|
13535
13940
|
}
|
|
13536
13941
|
// ============================================================================
|
|
13942
|
+
// Tenant Dispatch Keys (Two-Level Index)
|
|
13943
|
+
// ============================================================================
|
|
13944
|
+
dispatchKey(shardId) {
|
|
13945
|
+
return this.#buildKey("dispatch", shardId.toString());
|
|
13946
|
+
}
|
|
13947
|
+
tenantQueueIndexKey(tenantId) {
|
|
13948
|
+
return this.#buildKey("tenantq", tenantId);
|
|
13949
|
+
}
|
|
13950
|
+
// ============================================================================
|
|
13537
13951
|
// Dead Letter Queue Keys
|
|
13538
13952
|
// ============================================================================
|
|
13539
13953
|
deadLetterQueueKey(tenantId) {
|
|
@@ -13746,6 +14160,44 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
13746
14160
|
queues: t.queues
|
|
13747
14161
|
}));
|
|
13748
14162
|
}
|
|
14163
|
+
/**
|
|
14164
|
+
* Select queues using the two-level tenant dispatch index.
|
|
14165
|
+
*
|
|
14166
|
+
* Algorithm:
|
|
14167
|
+
* 1. ZRANGEBYSCORE on dispatch index (gets only tenants with queues - much smaller)
|
|
14168
|
+
* 2. Add quantum to each tenant's deficit (atomically)
|
|
14169
|
+
* 3. Check capacity as safety net (dispatch should only have tenants with capacity)
|
|
14170
|
+
* 4. Select tenants with deficit >= 1, sorted by deficit (highest first)
|
|
14171
|
+
* 5. For each tenant, fetch their queues from Level 2 index
|
|
14172
|
+
*/
|
|
14173
|
+
async selectQueuesFromDispatch(dispatchShardKey, consumerId, context2) {
|
|
14174
|
+
const tenants = await this.#getTenantsFromDispatch(dispatchShardKey);
|
|
14175
|
+
if (tenants.length === 0) {
|
|
14176
|
+
return [];
|
|
14177
|
+
}
|
|
14178
|
+
const tenantIds = tenants.map((t) => t.tenantId);
|
|
14179
|
+
const deficits = await this.#addQuantumToTenants(tenantIds);
|
|
14180
|
+
const candidates = tenantIds.map((tenantId, index) => ({ tenantId, deficit: deficits[index] ?? 0 })).filter((t) => t.deficit >= 1);
|
|
14181
|
+
candidates.sort((a, b) => b.deficit - a.deficit);
|
|
14182
|
+
for (const { tenantId, deficit } of candidates) {
|
|
14183
|
+
const isAtCapacity = await context2.isAtCapacity("tenant", tenantId);
|
|
14184
|
+
if (isAtCapacity) continue;
|
|
14185
|
+
const queueLimit = Math.ceil(deficit);
|
|
14186
|
+
const queues = await context2.getQueuesForTenant(tenantId, queueLimit);
|
|
14187
|
+
if (queues.length > 0) {
|
|
14188
|
+
this.logger.debug("DRR dispatch: selected tenant", {
|
|
14189
|
+
dispatchTenants: tenants.length,
|
|
14190
|
+
candidates: candidates.length,
|
|
14191
|
+
selectedTenant: tenantId,
|
|
14192
|
+
deficit,
|
|
14193
|
+
queueLimit,
|
|
14194
|
+
queuesReturned: queues.length
|
|
14195
|
+
});
|
|
14196
|
+
return [{ tenantId, queues: queues.map((q) => q.queueId) }];
|
|
14197
|
+
}
|
|
14198
|
+
}
|
|
14199
|
+
return [];
|
|
14200
|
+
}
|
|
13749
14201
|
/**
|
|
13750
14202
|
* Record that a message was processed from a tenant.
|
|
13751
14203
|
* Decrements the tenant's deficit.
|
|
@@ -13800,6 +14252,30 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
13800
14252
|
#deficitKey() {
|
|
13801
14253
|
return `${this.keys.masterQueueKey(0).split(":")[0]}:drr:deficit`;
|
|
13802
14254
|
}
|
|
14255
|
+
async #getTenantsFromDispatch(dispatchKey) {
|
|
14256
|
+
const now = Date.now();
|
|
14257
|
+
const results = await this.redis.zrangebyscore(
|
|
14258
|
+
dispatchKey,
|
|
14259
|
+
"-inf",
|
|
14260
|
+
now,
|
|
14261
|
+
"WITHSCORES",
|
|
14262
|
+
"LIMIT",
|
|
14263
|
+
0,
|
|
14264
|
+
this.masterQueueLimit
|
|
14265
|
+
);
|
|
14266
|
+
const tenants = [];
|
|
14267
|
+
for (let i = 0; i < results.length; i += 2) {
|
|
14268
|
+
const tenantId = results[i];
|
|
14269
|
+
const scoreStr = results[i + 1];
|
|
14270
|
+
if (tenantId && scoreStr) {
|
|
14271
|
+
tenants.push({
|
|
14272
|
+
tenantId,
|
|
14273
|
+
score: parseFloat(scoreStr)
|
|
14274
|
+
});
|
|
14275
|
+
}
|
|
14276
|
+
}
|
|
14277
|
+
return tenants;
|
|
14278
|
+
}
|
|
13803
14279
|
async #getQueuesFromShard(shardKey) {
|
|
13804
14280
|
const now = Date.now();
|
|
13805
14281
|
const results = await this.redis.zrangebyscore(
|
|
@@ -14433,7 +14909,8 @@ var FairQueue = class {
|
|
|
14433
14909
|
this.cooloffThreshold = options.cooloff?.threshold ?? 10;
|
|
14434
14910
|
this.cooloffPeriodMs = options.cooloff?.periodMs ?? 1e4;
|
|
14435
14911
|
this.maxCooloffStatesSize = options.cooloff?.maxStatesSize ?? 1e3;
|
|
14436
|
-
this.
|
|
14912
|
+
this.workerQueueMaxDepth = options.workerQueueMaxDepth ?? 0;
|
|
14913
|
+
this.workerQueueDepthCheckId = options.workerQueueDepthCheckId;
|
|
14437
14914
|
this.consumerTraceMaxIterations = options.consumerTraceMaxIterations ?? 500;
|
|
14438
14915
|
this.consumerTraceTimeoutSeconds = options.consumerTraceTimeoutSeconds ?? 60;
|
|
14439
14916
|
this.telemetry = new FairQueueTelemetry({
|
|
@@ -14456,6 +14933,11 @@ var FairQueue = class {
|
|
|
14456
14933
|
keys: options.keys,
|
|
14457
14934
|
shardCount: this.shardCount
|
|
14458
14935
|
});
|
|
14936
|
+
this.tenantDispatch = new TenantDispatch({
|
|
14937
|
+
redis: options.redis,
|
|
14938
|
+
keys: options.keys,
|
|
14939
|
+
shardCount: this.shardCount
|
|
14940
|
+
});
|
|
14459
14941
|
if (options.concurrencyGroups && options.concurrencyGroups.length > 0) {
|
|
14460
14942
|
this.concurrencyManager = new ConcurrencyManager({
|
|
14461
14943
|
redis: options.redis,
|
|
@@ -14514,8 +14996,9 @@ var FairQueue = class {
|
|
|
14514
14996
|
cooloffPeriodMs;
|
|
14515
14997
|
maxCooloffStatesSize;
|
|
14516
14998
|
queueCooloffStates = /* @__PURE__ */ new Map();
|
|
14517
|
-
//
|
|
14518
|
-
|
|
14999
|
+
// Worker queue backpressure
|
|
15000
|
+
workerQueueMaxDepth;
|
|
15001
|
+
workerQueueDepthCheckId;
|
|
14519
15002
|
// Consumer tracing
|
|
14520
15003
|
consumerTraceMaxIterations;
|
|
14521
15004
|
consumerTraceTimeoutSeconds;
|
|
@@ -14527,6 +15010,8 @@ var FairQueue = class {
|
|
|
14527
15010
|
reclaimLoop;
|
|
14528
15011
|
// Queue descriptor cache for message processing
|
|
14529
15012
|
queueDescriptorCache = /* @__PURE__ */ new Map();
|
|
15013
|
+
// Two-level tenant dispatch
|
|
15014
|
+
tenantDispatch;
|
|
14530
15015
|
// ============================================================================
|
|
14531
15016
|
// Public API - Telemetry
|
|
14532
15017
|
// ============================================================================
|
|
@@ -14541,6 +15026,9 @@ var FairQueue = class {
|
|
|
14541
15026
|
getMasterQueueLength: async (shardId) => {
|
|
14542
15027
|
return await this.masterQueue.getShardQueueCount(shardId);
|
|
14543
15028
|
},
|
|
15029
|
+
getDispatchLength: async (shardId) => {
|
|
15030
|
+
return await this.tenantDispatch.getShardTenantCount(shardId);
|
|
15031
|
+
},
|
|
14544
15032
|
getInflightCount: async (shardId) => {
|
|
14545
15033
|
return await this.visibilityManager.getInflightCount(shardId);
|
|
14546
15034
|
},
|
|
@@ -14565,8 +15053,9 @@ var FairQueue = class {
|
|
|
14565
15053
|
const timestamp = options.timestamp ?? Date.now();
|
|
14566
15054
|
const queueKey = this.keys.queueKey(options.queueId);
|
|
14567
15055
|
const queueItemsKey = this.keys.queueItemsKey(options.queueId);
|
|
14568
|
-
const
|
|
14569
|
-
const
|
|
15056
|
+
const dispatchShardId = this.tenantDispatch.getShardForTenant(options.tenantId);
|
|
15057
|
+
const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(options.tenantId);
|
|
15058
|
+
const dispatchKey = this.keys.dispatchKey(dispatchShardId);
|
|
14570
15059
|
if (this.validateOnEnqueue && this.payloadSchema) {
|
|
14571
15060
|
const result = this.payloadSchema.safeParse(options.payload);
|
|
14572
15061
|
if (!result.success) {
|
|
@@ -14597,20 +15086,22 @@ var FairQueue = class {
|
|
|
14597
15086
|
}) : void 0,
|
|
14598
15087
|
metadata: options.metadata
|
|
14599
15088
|
};
|
|
14600
|
-
await this.redis.
|
|
15089
|
+
await this.redis.enqueueMessageAtomicV2(
|
|
14601
15090
|
queueKey,
|
|
14602
15091
|
queueItemsKey,
|
|
14603
|
-
|
|
15092
|
+
tenantQueueIndexKey,
|
|
15093
|
+
dispatchKey,
|
|
14604
15094
|
options.queueId,
|
|
14605
15095
|
messageId,
|
|
14606
15096
|
timestamp.toString(),
|
|
14607
|
-
JSON.stringify(storedMessage)
|
|
15097
|
+
JSON.stringify(storedMessage),
|
|
15098
|
+
options.tenantId
|
|
14608
15099
|
);
|
|
14609
15100
|
span.setAttributes({
|
|
14610
15101
|
[FairQueueAttributes.QUEUE_ID]: options.queueId,
|
|
14611
15102
|
[FairQueueAttributes.TENANT_ID]: options.tenantId,
|
|
14612
15103
|
[FairQueueAttributes.MESSAGE_ID]: messageId,
|
|
14613
|
-
[FairQueueAttributes.SHARD_ID]:
|
|
15104
|
+
[FairQueueAttributes.SHARD_ID]: dispatchShardId.toString()
|
|
14614
15105
|
});
|
|
14615
15106
|
this.telemetry.recordEnqueue();
|
|
14616
15107
|
this.logger.debug("Message enqueued", {
|
|
@@ -14637,8 +15128,9 @@ var FairQueue = class {
|
|
|
14637
15128
|
async (span) => {
|
|
14638
15129
|
const queueKey = this.keys.queueKey(options.queueId);
|
|
14639
15130
|
const queueItemsKey = this.keys.queueItemsKey(options.queueId);
|
|
14640
|
-
const
|
|
14641
|
-
const
|
|
15131
|
+
const dispatchShardId = this.tenantDispatch.getShardForTenant(options.tenantId);
|
|
15132
|
+
const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(options.tenantId);
|
|
15133
|
+
const dispatchKey = this.keys.dispatchKey(dispatchShardId);
|
|
14642
15134
|
const now = Date.now();
|
|
14643
15135
|
const descriptor = {
|
|
14644
15136
|
id: options.queueId,
|
|
@@ -14680,18 +15172,20 @@ var FairQueue = class {
|
|
|
14680
15172
|
messageIds.push(messageId);
|
|
14681
15173
|
args.push(messageId, timestamp.toString(), JSON.stringify(storedMessage));
|
|
14682
15174
|
}
|
|
14683
|
-
await this.redis.
|
|
15175
|
+
await this.redis.enqueueBatchAtomicV2(
|
|
14684
15176
|
queueKey,
|
|
14685
15177
|
queueItemsKey,
|
|
14686
|
-
|
|
15178
|
+
tenantQueueIndexKey,
|
|
15179
|
+
dispatchKey,
|
|
14687
15180
|
options.queueId,
|
|
15181
|
+
options.tenantId,
|
|
14688
15182
|
...args
|
|
14689
15183
|
);
|
|
14690
15184
|
span.setAttributes({
|
|
14691
15185
|
[FairQueueAttributes.QUEUE_ID]: options.queueId,
|
|
14692
15186
|
[FairQueueAttributes.TENANT_ID]: options.tenantId,
|
|
14693
15187
|
[FairQueueAttributes.MESSAGE_COUNT]: messageIds.length,
|
|
14694
|
-
[FairQueueAttributes.SHARD_ID]:
|
|
15188
|
+
[FairQueueAttributes.SHARD_ID]: dispatchShardId.toString()
|
|
14695
15189
|
});
|
|
14696
15190
|
this.telemetry.recordEnqueueBatch(messageIds.length);
|
|
14697
15191
|
this.logger.debug("Batch enqueued", {
|
|
@@ -14893,6 +15387,7 @@ var FairQueue = class {
|
|
|
14893
15387
|
this.batchedSpanManager.cleanupAll();
|
|
14894
15388
|
await Promise.all([
|
|
14895
15389
|
this.masterQueue.close(),
|
|
15390
|
+
this.tenantDispatch.close(),
|
|
14896
15391
|
this.concurrencyManager?.close(),
|
|
14897
15392
|
this.visibilityManager.close(),
|
|
14898
15393
|
this.workerQueueManager.close(),
|
|
@@ -14911,10 +15406,14 @@ var FairQueue = class {
|
|
|
14911
15406
|
return await this.redis.zcard(queueKey);
|
|
14912
15407
|
}
|
|
14913
15408
|
/**
|
|
14914
|
-
* Get total
|
|
15409
|
+
* Get total tenant count across dispatch shards plus any legacy queues still draining.
|
|
14915
15410
|
*/
|
|
14916
15411
|
async getTotalQueueCount() {
|
|
14917
|
-
|
|
15412
|
+
const [dispatchCount, legacyCount] = await Promise.all([
|
|
15413
|
+
this.tenantDispatch.getTotalTenantCount(),
|
|
15414
|
+
this.masterQueue.getTotalQueueCount()
|
|
15415
|
+
]);
|
|
15416
|
+
return dispatchCount + legacyCount;
|
|
14918
15417
|
}
|
|
14919
15418
|
/**
|
|
14920
15419
|
* Get total in-flight message count.
|
|
@@ -14945,7 +15444,7 @@ var FairQueue = class {
|
|
|
14945
15444
|
loopId,
|
|
14946
15445
|
async (span) => {
|
|
14947
15446
|
span.setAttribute("shard_id", shardId);
|
|
14948
|
-
return await this.#
|
|
15447
|
+
return await this.#processShardIteration(loopId, shardId, span);
|
|
14949
15448
|
},
|
|
14950
15449
|
{
|
|
14951
15450
|
iterationSpanName: "processMasterQueueShard",
|
|
@@ -14984,32 +15483,142 @@ var FairQueue = class {
|
|
|
14984
15483
|
this.batchedSpanManager.cleanup(loopId);
|
|
14985
15484
|
}
|
|
14986
15485
|
}
|
|
14987
|
-
|
|
15486
|
+
/**
|
|
15487
|
+
* Process a shard iteration. Runs both the new tenant dispatch path
|
|
15488
|
+
* and the legacy master queue drain path.
|
|
15489
|
+
*/
|
|
15490
|
+
async #processShardIteration(loopId, shardId, parentSpan) {
|
|
15491
|
+
let hadWork = false;
|
|
15492
|
+
hadWork = await this.#processDispatchShard(loopId, shardId, parentSpan);
|
|
15493
|
+
const legacyCount = await this.masterQueue.getShardQueueCount(shardId);
|
|
15494
|
+
if (legacyCount > 0) {
|
|
15495
|
+
const drainHadWork = await this.#drainLegacyMasterQueueShard(loopId, shardId, parentSpan);
|
|
15496
|
+
hadWork = hadWork || drainHadWork;
|
|
15497
|
+
}
|
|
15498
|
+
return hadWork;
|
|
15499
|
+
}
|
|
15500
|
+
/**
|
|
15501
|
+
* Main path: process queues using the two-level tenant dispatch index.
|
|
15502
|
+
* Level 1: dispatch index → tenantIds. Level 2: per-tenant → queueIds.
|
|
15503
|
+
*/
|
|
15504
|
+
async #processDispatchShard(loopId, shardId, parentSpan) {
|
|
15505
|
+
const dispatchKey = this.keys.dispatchKey(shardId);
|
|
15506
|
+
const dispatchSize = await this.tenantDispatch.getShardTenantCount(shardId);
|
|
15507
|
+
parentSpan?.setAttribute("dispatch_size", dispatchSize);
|
|
15508
|
+
this.batchedSpanManager.incrementStat(loopId, "dispatch_size_sum", dispatchSize);
|
|
15509
|
+
const schedulerContext = {
|
|
15510
|
+
...this.#createSchedulerContext(),
|
|
15511
|
+
getQueuesForTenant: async (tenantId, limit) => {
|
|
15512
|
+
return this.tenantDispatch.getQueuesForTenant(tenantId, limit);
|
|
15513
|
+
}
|
|
15514
|
+
};
|
|
15515
|
+
let tenantQueues;
|
|
15516
|
+
if (this.scheduler.selectQueuesFromDispatch) {
|
|
15517
|
+
tenantQueues = await this.telemetry.trace(
|
|
15518
|
+
"selectQueuesFromDispatch",
|
|
15519
|
+
async (span) => {
|
|
15520
|
+
span.setAttribute(FairQueueAttributes.SHARD_ID, shardId.toString());
|
|
15521
|
+
span.setAttribute(FairQueueAttributes.CONSUMER_ID, loopId);
|
|
15522
|
+
span.setAttribute("dispatch_size", dispatchSize);
|
|
15523
|
+
const result = await this.scheduler.selectQueuesFromDispatch(
|
|
15524
|
+
dispatchKey,
|
|
15525
|
+
loopId,
|
|
15526
|
+
schedulerContext
|
|
15527
|
+
);
|
|
15528
|
+
span.setAttribute("tenant_count", result.length);
|
|
15529
|
+
span.setAttribute(
|
|
15530
|
+
"queue_count",
|
|
15531
|
+
result.reduce((acc, t) => acc + t.queues.length, 0)
|
|
15532
|
+
);
|
|
15533
|
+
return result;
|
|
15534
|
+
},
|
|
15535
|
+
{ kind: SpanKind.INTERNAL }
|
|
15536
|
+
);
|
|
15537
|
+
} else {
|
|
15538
|
+
tenantQueues = await this.#fallbackDispatchToLegacyScheduler(
|
|
15539
|
+
loopId,
|
|
15540
|
+
shardId,
|
|
15541
|
+
schedulerContext,
|
|
15542
|
+
parentSpan
|
|
15543
|
+
);
|
|
15544
|
+
}
|
|
15545
|
+
if (tenantQueues.length === 0) {
|
|
15546
|
+
this.batchedSpanManager.incrementStat(loopId, "empty_iterations");
|
|
15547
|
+
return false;
|
|
15548
|
+
}
|
|
15549
|
+
return this.#processSelectedQueues(loopId, shardId, tenantQueues);
|
|
15550
|
+
}
|
|
15551
|
+
/**
|
|
15552
|
+
* Drain path: process remaining messages from the legacy master queue shard.
|
|
15553
|
+
* Uses simple ZRANGEBYSCORE without DRR - just flushing pre-deploy messages.
|
|
15554
|
+
*/
|
|
15555
|
+
async #drainLegacyMasterQueueShard(loopId, shardId, parentSpan) {
|
|
14988
15556
|
const masterQueueKey = this.keys.masterQueueKey(shardId);
|
|
14989
|
-
const
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
"
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
span.setAttribute("master_queue_size", masterQueueSize);
|
|
14999
|
-
const result = await this.scheduler.selectQueues(masterQueueKey, loopId, schedulerContext);
|
|
15000
|
-
span.setAttribute("tenant_count", result.length);
|
|
15001
|
-
span.setAttribute(
|
|
15002
|
-
"queue_count",
|
|
15003
|
-
result.reduce((acc, t) => acc + t.queues.length, 0)
|
|
15004
|
-
);
|
|
15005
|
-
return result;
|
|
15006
|
-
},
|
|
15007
|
-
{ kind: SpanKind.INTERNAL }
|
|
15557
|
+
const now = Date.now();
|
|
15558
|
+
const results = await this.redis.zrangebyscore(
|
|
15559
|
+
masterQueueKey,
|
|
15560
|
+
"-inf",
|
|
15561
|
+
now,
|
|
15562
|
+
"WITHSCORES",
|
|
15563
|
+
"LIMIT",
|
|
15564
|
+
0,
|
|
15565
|
+
100
|
|
15008
15566
|
);
|
|
15567
|
+
if (results.length === 0) {
|
|
15568
|
+
return false;
|
|
15569
|
+
}
|
|
15570
|
+
const byTenant = /* @__PURE__ */ new Map();
|
|
15571
|
+
for (let i = 0; i < results.length; i += 2) {
|
|
15572
|
+
const queueId = results[i];
|
|
15573
|
+
const _score = results[i + 1];
|
|
15574
|
+
if (queueId && _score) {
|
|
15575
|
+
const tenantId = this.keys.extractTenantId(queueId);
|
|
15576
|
+
const existing = byTenant.get(tenantId) ?? [];
|
|
15577
|
+
existing.push(queueId);
|
|
15578
|
+
byTenant.set(tenantId, existing);
|
|
15579
|
+
}
|
|
15580
|
+
}
|
|
15581
|
+
const tenantQueues = [];
|
|
15582
|
+
for (const [tenantId, queueIds] of byTenant) {
|
|
15583
|
+
if (this.concurrencyManager) {
|
|
15584
|
+
const atCapacity = await this.concurrencyManager.isAtCapacity("tenant", tenantId);
|
|
15585
|
+
if (atCapacity) continue;
|
|
15586
|
+
}
|
|
15587
|
+
tenantQueues.push({ tenantId, queues: queueIds });
|
|
15588
|
+
}
|
|
15009
15589
|
if (tenantQueues.length === 0) {
|
|
15010
|
-
this.batchedSpanManager.incrementStat(loopId, "empty_iterations");
|
|
15011
15590
|
return false;
|
|
15012
15591
|
}
|
|
15592
|
+
parentSpan?.setAttribute("drain_tenants", tenantQueues.length);
|
|
15593
|
+
this.batchedSpanManager.incrementStat(loopId, "drain_tenants", tenantQueues.length);
|
|
15594
|
+
return this.#processSelectedQueues(loopId, shardId, tenantQueues);
|
|
15595
|
+
}
|
|
15596
|
+
/**
|
|
15597
|
+
* Fallback for schedulers that don't implement selectQueuesFromDispatch.
|
|
15598
|
+
* Reads dispatch index, fetches per-tenant queues, groups by tenant,
|
|
15599
|
+
* and filters at-capacity tenants. No DRR deficit tracking in this path.
|
|
15600
|
+
*/
|
|
15601
|
+
async #fallbackDispatchToLegacyScheduler(loopId, shardId, context2, parentSpan) {
|
|
15602
|
+
const tenants = await this.tenantDispatch.getTenantsFromShard(shardId);
|
|
15603
|
+
if (tenants.length === 0) return [];
|
|
15604
|
+
const tenantQueues = [];
|
|
15605
|
+
for (const { tenantId } of tenants) {
|
|
15606
|
+
if (this.concurrencyManager) {
|
|
15607
|
+
const atCapacity = await this.concurrencyManager.isAtCapacity("tenant", tenantId);
|
|
15608
|
+
if (atCapacity) continue;
|
|
15609
|
+
}
|
|
15610
|
+
const queues = await this.tenantDispatch.getQueuesForTenant(tenantId);
|
|
15611
|
+
if (queues.length > 0) {
|
|
15612
|
+
tenantQueues.push({ tenantId, queues: queues.map((q) => q.queueId) });
|
|
15613
|
+
}
|
|
15614
|
+
}
|
|
15615
|
+
return tenantQueues;
|
|
15616
|
+
}
|
|
15617
|
+
/**
|
|
15618
|
+
* Shared claim loop: process selected queues from either dispatch or drain path.
|
|
15619
|
+
* Claims messages and pushes to worker queues.
|
|
15620
|
+
*/
|
|
15621
|
+
async #processSelectedQueues(loopId, shardId, tenantQueues) {
|
|
15013
15622
|
this.batchedSpanManager.incrementStat(loopId, "tenants_selected", tenantQueues.length);
|
|
15014
15623
|
this.batchedSpanManager.incrementStat(
|
|
15015
15624
|
loopId,
|
|
@@ -15076,10 +15685,10 @@ var FairQueue = class {
|
|
|
15076
15685
|
}
|
|
15077
15686
|
return messagesProcessed > 0;
|
|
15078
15687
|
}
|
|
15079
|
-
async #claimAndPushToWorkerQueue(loopId, queueId, tenantId,
|
|
15688
|
+
async #claimAndPushToWorkerQueue(loopId, queueId, tenantId, _consumerShardId) {
|
|
15689
|
+
const dispatchShardId = this.tenantDispatch.getShardForTenant(tenantId);
|
|
15080
15690
|
const queueKey = this.keys.queueKey(queueId);
|
|
15081
15691
|
const queueItemsKey = this.keys.queueItemsKey(queueId);
|
|
15082
|
-
const masterQueueKey = this.keys.masterQueueKey(shardId);
|
|
15083
15692
|
const descriptor = this.queueDescriptorCache.get(queueId) ?? {
|
|
15084
15693
|
id: queueId,
|
|
15085
15694
|
tenantId,
|
|
@@ -15089,28 +15698,23 @@ var FairQueue = class {
|
|
|
15089
15698
|
if (this.concurrencyManager) {
|
|
15090
15699
|
const availableCapacity = await this.concurrencyManager.getAvailableCapacity(descriptor);
|
|
15091
15700
|
if (availableCapacity === 0) {
|
|
15092
|
-
this.#incrementCooloff(queueId);
|
|
15093
15701
|
return 0;
|
|
15094
15702
|
}
|
|
15095
15703
|
maxClaimCount = Math.min(maxClaimCount, availableCapacity);
|
|
15096
15704
|
}
|
|
15097
|
-
if (this.
|
|
15098
|
-
const
|
|
15099
|
-
if (
|
|
15100
|
-
|
|
15101
|
-
if (waitMs > 0) {
|
|
15102
|
-
this.logger.debug("Global rate limit reached, waiting", { waitMs, loopId });
|
|
15103
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
15104
|
-
}
|
|
15705
|
+
if (this.workerQueueMaxDepth > 0 && this.workerQueueDepthCheckId) {
|
|
15706
|
+
const depth = await this.workerQueueManager.getLength(this.workerQueueDepthCheckId);
|
|
15707
|
+
if (depth >= this.workerQueueMaxDepth) {
|
|
15708
|
+
return 0;
|
|
15105
15709
|
}
|
|
15710
|
+
const remainingCapacity = this.workerQueueMaxDepth - depth;
|
|
15711
|
+
maxClaimCount = Math.min(maxClaimCount, remainingCapacity);
|
|
15106
15712
|
}
|
|
15107
15713
|
const claimedMessages = await this.visibilityManager.claimBatch(queueId, queueKey, queueItemsKey, loopId, maxClaimCount, this.visibilityTimeoutMs);
|
|
15108
15714
|
if (claimedMessages.length === 0) {
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
this.queueCooloffStates.delete(queueId);
|
|
15113
|
-
}
|
|
15715
|
+
await this.#updateAllIndexesAfterDequeue(queueId, tenantId);
|
|
15716
|
+
this.queueDescriptorCache.delete(queueId);
|
|
15717
|
+
this.queueCooloffStates.delete(queueId);
|
|
15114
15718
|
return 0;
|
|
15115
15719
|
}
|
|
15116
15720
|
let processedCount = 0;
|
|
@@ -15119,12 +15723,16 @@ var FairQueue = class {
|
|
|
15119
15723
|
if (this.concurrencyManager) {
|
|
15120
15724
|
const reserved = await this.concurrencyManager.reserve(descriptor, message.messageId);
|
|
15121
15725
|
if (!reserved) {
|
|
15726
|
+
const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(tenantId);
|
|
15727
|
+
const dispatchKey = this.keys.dispatchKey(dispatchShardId);
|
|
15122
15728
|
await this.visibilityManager.releaseBatch(
|
|
15123
15729
|
claimedMessages.slice(i),
|
|
15124
15730
|
queueId,
|
|
15125
15731
|
queueKey,
|
|
15126
15732
|
queueItemsKey,
|
|
15127
|
-
|
|
15733
|
+
tenantQueueIndexKey,
|
|
15734
|
+
dispatchKey,
|
|
15735
|
+
tenantId
|
|
15128
15736
|
);
|
|
15129
15737
|
break;
|
|
15130
15738
|
}
|
|
@@ -15184,8 +15792,6 @@ var FairQueue = class {
|
|
|
15184
15792
|
*/
|
|
15185
15793
|
async completeMessage(messageId, queueId) {
|
|
15186
15794
|
const shardId = this.masterQueue.getShardForQueue(queueId);
|
|
15187
|
-
const queueKey = this.keys.queueKey(queueId);
|
|
15188
|
-
const masterQueueKey = this.keys.masterQueueKey(shardId);
|
|
15189
15795
|
const inflightDataKey = this.keys.inflightDataKey(shardId);
|
|
15190
15796
|
const dataJson = await this.redis.hget(inflightDataKey, messageId);
|
|
15191
15797
|
let storedMessage = null;
|
|
@@ -15199,13 +15805,16 @@ var FairQueue = class {
|
|
|
15199
15805
|
id: queueId,
|
|
15200
15806
|
tenantId: storedMessage.tenantId,
|
|
15201
15807
|
metadata: storedMessage.metadata ?? {}
|
|
15202
|
-
} : { id: queueId, tenantId:
|
|
15808
|
+
} : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} };
|
|
15203
15809
|
await this.visibilityManager.complete(messageId, queueId);
|
|
15204
15810
|
if (this.concurrencyManager && storedMessage) {
|
|
15205
15811
|
await this.concurrencyManager.release(descriptor, messageId);
|
|
15206
15812
|
}
|
|
15207
|
-
const
|
|
15208
|
-
|
|
15813
|
+
const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(
|
|
15814
|
+
queueId,
|
|
15815
|
+
descriptor.tenantId
|
|
15816
|
+
);
|
|
15817
|
+
if (queueEmpty) {
|
|
15209
15818
|
this.queueDescriptorCache.delete(queueId);
|
|
15210
15819
|
this.queueCooloffStates.delete(queueId);
|
|
15211
15820
|
}
|
|
@@ -15226,7 +15835,6 @@ var FairQueue = class {
|
|
|
15226
15835
|
const shardId = this.masterQueue.getShardForQueue(queueId);
|
|
15227
15836
|
const queueKey = this.keys.queueKey(queueId);
|
|
15228
15837
|
const queueItemsKey = this.keys.queueItemsKey(queueId);
|
|
15229
|
-
const masterQueueKey = this.keys.masterQueueKey(shardId);
|
|
15230
15838
|
const inflightDataKey = this.keys.inflightDataKey(shardId);
|
|
15231
15839
|
const dataJson = await this.redis.hget(inflightDataKey, messageId);
|
|
15232
15840
|
let storedMessage = null;
|
|
@@ -15240,13 +15848,18 @@ var FairQueue = class {
|
|
|
15240
15848
|
id: queueId,
|
|
15241
15849
|
tenantId: storedMessage.tenantId,
|
|
15242
15850
|
metadata: storedMessage.metadata ?? {}
|
|
15243
|
-
} : { id: queueId, tenantId:
|
|
15851
|
+
} : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} };
|
|
15852
|
+
const dispatchShardId = this.tenantDispatch.getShardForTenant(descriptor.tenantId);
|
|
15853
|
+
const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(descriptor.tenantId);
|
|
15854
|
+
const dispatchKey = this.keys.dispatchKey(dispatchShardId);
|
|
15244
15855
|
await this.visibilityManager.release(
|
|
15245
15856
|
messageId,
|
|
15246
15857
|
queueId,
|
|
15247
15858
|
queueKey,
|
|
15248
15859
|
queueItemsKey,
|
|
15249
|
-
|
|
15860
|
+
tenantQueueIndexKey,
|
|
15861
|
+
dispatchKey,
|
|
15862
|
+
descriptor.tenantId,
|
|
15250
15863
|
Date.now()
|
|
15251
15864
|
// Put at back of queue
|
|
15252
15865
|
);
|
|
@@ -15270,7 +15883,6 @@ var FairQueue = class {
|
|
|
15270
15883
|
const shardId = this.masterQueue.getShardForQueue(queueId);
|
|
15271
15884
|
const queueKey = this.keys.queueKey(queueId);
|
|
15272
15885
|
const queueItemsKey = this.keys.queueItemsKey(queueId);
|
|
15273
|
-
const masterQueueKey = this.keys.masterQueueKey(shardId);
|
|
15274
15886
|
const inflightDataKey = this.keys.inflightDataKey(shardId);
|
|
15275
15887
|
const dataJson = await this.redis.hget(inflightDataKey, messageId);
|
|
15276
15888
|
if (!dataJson) {
|
|
@@ -15292,12 +15904,13 @@ var FairQueue = class {
|
|
|
15292
15904
|
tenantId: storedMessage.tenantId,
|
|
15293
15905
|
metadata: storedMessage.metadata ?? {}
|
|
15294
15906
|
};
|
|
15907
|
+
const dispatchShardId = this.tenantDispatch.getShardForTenant(descriptor.tenantId);
|
|
15295
15908
|
await this.#handleMessageFailure(
|
|
15296
15909
|
storedMessage,
|
|
15297
15910
|
queueId,
|
|
15298
15911
|
queueKey,
|
|
15299
15912
|
queueItemsKey,
|
|
15300
|
-
|
|
15913
|
+
dispatchShardId,
|
|
15301
15914
|
descriptor,
|
|
15302
15915
|
error
|
|
15303
15916
|
);
|
|
@@ -15305,7 +15918,7 @@ var FairQueue = class {
|
|
|
15305
15918
|
// ============================================================================
|
|
15306
15919
|
// Private - Message Processing Helpers
|
|
15307
15920
|
// ============================================================================
|
|
15308
|
-
async #handleMessageFailure(storedMessage, queueId, queueKey, queueItemsKey,
|
|
15921
|
+
async #handleMessageFailure(storedMessage, queueId, queueKey, queueItemsKey, dispatchShardId, descriptor, error) {
|
|
15309
15922
|
this.telemetry.recordFailure();
|
|
15310
15923
|
if (this.retryStrategy) {
|
|
15311
15924
|
const nextDelay = this.retryStrategy.getNextDelay(storedMessage.attempt, error);
|
|
@@ -15314,15 +15927,19 @@ var FairQueue = class {
|
|
|
15314
15927
|
...storedMessage,
|
|
15315
15928
|
attempt: storedMessage.attempt + 1
|
|
15316
15929
|
};
|
|
15930
|
+
const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(descriptor.tenantId);
|
|
15931
|
+
const dispatchKey = this.keys.dispatchKey(dispatchShardId);
|
|
15317
15932
|
await this.visibilityManager.release(
|
|
15318
15933
|
storedMessage.id,
|
|
15319
15934
|
queueId,
|
|
15320
15935
|
queueKey,
|
|
15321
15936
|
queueItemsKey,
|
|
15322
|
-
|
|
15323
|
-
|
|
15937
|
+
tenantQueueIndexKey,
|
|
15938
|
+
dispatchKey,
|
|
15939
|
+
descriptor.tenantId,
|
|
15940
|
+
Date.now() + nextDelay,
|
|
15941
|
+
JSON.stringify(updatedMessage)
|
|
15324
15942
|
);
|
|
15325
|
-
await this.redis.hset(queueItemsKey, storedMessage.id, JSON.stringify(updatedMessage));
|
|
15326
15943
|
if (this.concurrencyManager) {
|
|
15327
15944
|
await this.concurrencyManager.release(descriptor, storedMessage.id);
|
|
15328
15945
|
}
|
|
@@ -15400,28 +16017,36 @@ var FairQueue = class {
|
|
|
15400
16017
|
async #reclaimTimedOutMessages() {
|
|
15401
16018
|
let totalReclaimed = 0;
|
|
15402
16019
|
for (let shardId = 0; shardId < this.shardCount; shardId++) {
|
|
15403
|
-
const reclaimedMessages = await this.visibilityManager.reclaimTimedOut(shardId, (queueId) =>
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
|
|
15415
|
-
|
|
15416
|
-
|
|
15417
|
-
|
|
15418
|
-
|
|
15419
|
-
|
|
15420
|
-
|
|
15421
|
-
|
|
15422
|
-
|
|
15423
|
-
|
|
15424
|
-
|
|
16020
|
+
const reclaimedMessages = await this.visibilityManager.reclaimTimedOut(shardId, (queueId) => {
|
|
16021
|
+
const tenantId = this.keys.extractTenantId(queueId);
|
|
16022
|
+
const dispatchShardId = this.tenantDispatch.getShardForTenant(tenantId);
|
|
16023
|
+
return {
|
|
16024
|
+
queueKey: this.keys.queueKey(queueId),
|
|
16025
|
+
queueItemsKey: this.keys.queueItemsKey(queueId),
|
|
16026
|
+
tenantQueueIndexKey: this.keys.tenantQueueIndexKey(tenantId),
|
|
16027
|
+
dispatchKey: this.keys.dispatchKey(dispatchShardId),
|
|
16028
|
+
tenantId
|
|
16029
|
+
};
|
|
16030
|
+
});
|
|
16031
|
+
if (reclaimedMessages.length > 0) {
|
|
16032
|
+
if (this.concurrencyManager) {
|
|
16033
|
+
try {
|
|
16034
|
+
await this.concurrencyManager.releaseBatch(
|
|
16035
|
+
reclaimedMessages.map((msg) => ({
|
|
16036
|
+
queue: {
|
|
16037
|
+
id: msg.queueId,
|
|
16038
|
+
tenantId: msg.tenantId,
|
|
16039
|
+
metadata: msg.metadata ?? {}
|
|
16040
|
+
},
|
|
16041
|
+
messageId: msg.messageId
|
|
16042
|
+
}))
|
|
16043
|
+
);
|
|
16044
|
+
} catch (error) {
|
|
16045
|
+
this.logger.error("Failed to release concurrency for reclaimed messages", {
|
|
16046
|
+
count: reclaimedMessages.length,
|
|
16047
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16048
|
+
});
|
|
16049
|
+
}
|
|
15425
16050
|
}
|
|
15426
16051
|
}
|
|
15427
16052
|
totalReclaimed += reclaimedMessages.length;
|
|
@@ -15483,6 +16108,32 @@ var FairQueue = class {
|
|
|
15483
16108
|
// ============================================================================
|
|
15484
16109
|
// Private - Helpers
|
|
15485
16110
|
// ============================================================================
|
|
16111
|
+
/**
|
|
16112
|
+
* Update both old master queue and new dispatch indexes after a dequeue/complete.
|
|
16113
|
+
* Both calls are idempotent - ZREM on a non-existent member is a no-op.
|
|
16114
|
+
* This handles the transition period where queues may exist in either or both indexes.
|
|
16115
|
+
*/
|
|
16116
|
+
async #updateAllIndexesAfterDequeue(queueId, tenantId) {
|
|
16117
|
+
const queueShardId = this.masterQueue.getShardForQueue(queueId);
|
|
16118
|
+
const dispatchShardId = this.tenantDispatch.getShardForTenant(tenantId);
|
|
16119
|
+
const queueKey = this.keys.queueKey(queueId);
|
|
16120
|
+
const masterQueueKey = this.keys.masterQueueKey(queueShardId);
|
|
16121
|
+
const tenantQueueIndexKey = this.keys.tenantQueueIndexKey(tenantId);
|
|
16122
|
+
const dispatchKey = this.keys.dispatchKey(dispatchShardId);
|
|
16123
|
+
const removedFromMaster = await this.redis.updateMasterQueueIfEmpty(
|
|
16124
|
+
masterQueueKey,
|
|
16125
|
+
queueKey,
|
|
16126
|
+
queueId
|
|
16127
|
+
);
|
|
16128
|
+
const removedFromDispatch = await this.redis.updateDispatchIndexes(
|
|
16129
|
+
queueKey,
|
|
16130
|
+
tenantQueueIndexKey,
|
|
16131
|
+
dispatchKey,
|
|
16132
|
+
queueId,
|
|
16133
|
+
tenantId
|
|
16134
|
+
);
|
|
16135
|
+
return { queueEmpty: removedFromMaster === 1 || removedFromDispatch === 1 };
|
|
16136
|
+
}
|
|
15486
16137
|
#createSchedulerContext() {
|
|
15487
16138
|
return {
|
|
15488
16139
|
getCurrentConcurrency: async (groupName, groupId) => {
|
|
@@ -15522,13 +16173,9 @@ local messageId = ARGV[2]
|
|
|
15522
16173
|
local timestamp = tonumber(ARGV[3])
|
|
15523
16174
|
local payload = ARGV[4]
|
|
15524
16175
|
|
|
15525
|
-
-- Add to sorted set (score = timestamp)
|
|
15526
16176
|
redis.call('ZADD', queueKey, timestamp, messageId)
|
|
15527
|
-
|
|
15528
|
-
-- Store payload in hash
|
|
15529
16177
|
redis.call('HSET', queueItemsKey, messageId, payload)
|
|
15530
16178
|
|
|
15531
|
-
-- Update master queue with oldest message timestamp
|
|
15532
16179
|
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
15533
16180
|
if #oldest >= 2 then
|
|
15534
16181
|
redis.call('ZADD', masterQueueKey, oldest[2], queueId)
|
|
@@ -15546,20 +16193,14 @@ local masterQueueKey = KEYS[3]
|
|
|
15546
16193
|
|
|
15547
16194
|
local queueId = ARGV[1]
|
|
15548
16195
|
|
|
15549
|
-
-- Args after queueId are triples: [messageId, timestamp, payload, ...]
|
|
15550
16196
|
for i = 2, #ARGV, 3 do
|
|
15551
16197
|
local messageId = ARGV[i]
|
|
15552
16198
|
local timestamp = tonumber(ARGV[i + 1])
|
|
15553
16199
|
local payload = ARGV[i + 2]
|
|
15554
|
-
|
|
15555
|
-
-- Add to sorted set
|
|
15556
16200
|
redis.call('ZADD', queueKey, timestamp, messageId)
|
|
15557
|
-
|
|
15558
|
-
-- Store payload in hash
|
|
15559
16201
|
redis.call('HSET', queueItemsKey, messageId, payload)
|
|
15560
16202
|
end
|
|
15561
16203
|
|
|
15562
|
-
-- Update master queue with oldest message timestamp
|
|
15563
16204
|
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
15564
16205
|
if #oldest >= 2 then
|
|
15565
16206
|
redis.call('ZADD', masterQueueKey, oldest[2], queueId)
|
|
@@ -15579,11 +16220,119 @@ local count = redis.call('ZCARD', queueKey)
|
|
|
15579
16220
|
if count == 0 then
|
|
15580
16221
|
redis.call('ZREM', masterQueueKey, queueId)
|
|
15581
16222
|
return 1
|
|
16223
|
+
end
|
|
16224
|
+
|
|
16225
|
+
-- Queue still has messages but don't re-add to legacy master queue.
|
|
16226
|
+
-- New enqueues go through the V2 dispatch path, so we only drain here.
|
|
16227
|
+
-- Just remove it so it doesn't linger.
|
|
16228
|
+
redis.call('ZREM', masterQueueKey, queueId)
|
|
16229
|
+
return 0
|
|
16230
|
+
`
|
|
16231
|
+
});
|
|
16232
|
+
this.redis.defineCommand("enqueueMessageAtomicV2", {
|
|
16233
|
+
numberOfKeys: 4,
|
|
16234
|
+
lua: `
|
|
16235
|
+
local queueKey = KEYS[1]
|
|
16236
|
+
local queueItemsKey = KEYS[2]
|
|
16237
|
+
local tenantQueueIndexKey = KEYS[3]
|
|
16238
|
+
local dispatchKey = KEYS[4]
|
|
16239
|
+
|
|
16240
|
+
local queueId = ARGV[1]
|
|
16241
|
+
local messageId = ARGV[2]
|
|
16242
|
+
local timestamp = tonumber(ARGV[3])
|
|
16243
|
+
local payload = ARGV[4]
|
|
16244
|
+
local tenantId = ARGV[5]
|
|
16245
|
+
|
|
16246
|
+
-- Add to per-queue storage (same as before)
|
|
16247
|
+
redis.call('ZADD', queueKey, timestamp, messageId)
|
|
16248
|
+
redis.call('HSET', queueItemsKey, messageId, payload)
|
|
16249
|
+
|
|
16250
|
+
-- Update tenant queue index (Level 2) with queue's oldest message
|
|
16251
|
+
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
16252
|
+
if #oldest >= 2 then
|
|
16253
|
+
redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
|
|
16254
|
+
end
|
|
16255
|
+
|
|
16256
|
+
-- Update dispatch index (Level 1) with tenant's oldest across all queues
|
|
16257
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
16258
|
+
if #tenantOldest >= 2 then
|
|
16259
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
16260
|
+
end
|
|
16261
|
+
|
|
16262
|
+
return 1
|
|
16263
|
+
`
|
|
16264
|
+
});
|
|
16265
|
+
this.redis.defineCommand("enqueueBatchAtomicV2", {
|
|
16266
|
+
numberOfKeys: 4,
|
|
16267
|
+
lua: `
|
|
16268
|
+
local queueKey = KEYS[1]
|
|
16269
|
+
local queueItemsKey = KEYS[2]
|
|
16270
|
+
local tenantQueueIndexKey = KEYS[3]
|
|
16271
|
+
local dispatchKey = KEYS[4]
|
|
16272
|
+
|
|
16273
|
+
local queueId = ARGV[1]
|
|
16274
|
+
local tenantId = ARGV[2]
|
|
16275
|
+
|
|
16276
|
+
-- Args after queueId and tenantId are triples: [messageId, timestamp, payload, ...]
|
|
16277
|
+
for i = 3, #ARGV, 3 do
|
|
16278
|
+
local messageId = ARGV[i]
|
|
16279
|
+
local timestamp = tonumber(ARGV[i + 1])
|
|
16280
|
+
local payload = ARGV[i + 2]
|
|
16281
|
+
redis.call('ZADD', queueKey, timestamp, messageId)
|
|
16282
|
+
redis.call('HSET', queueItemsKey, messageId, payload)
|
|
16283
|
+
end
|
|
16284
|
+
|
|
16285
|
+
-- Update tenant queue index (Level 2)
|
|
16286
|
+
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
16287
|
+
if #oldest >= 2 then
|
|
16288
|
+
redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
|
|
16289
|
+
end
|
|
16290
|
+
|
|
16291
|
+
-- Update dispatch index (Level 1)
|
|
16292
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
16293
|
+
if #tenantOldest >= 2 then
|
|
16294
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
16295
|
+
end
|
|
16296
|
+
|
|
16297
|
+
return (#ARGV - 2) / 3
|
|
16298
|
+
`
|
|
16299
|
+
});
|
|
16300
|
+
this.redis.defineCommand("updateDispatchIndexes", {
|
|
16301
|
+
numberOfKeys: 3,
|
|
16302
|
+
lua: `
|
|
16303
|
+
local queueKey = KEYS[1]
|
|
16304
|
+
local tenantQueueIndexKey = KEYS[2]
|
|
16305
|
+
local dispatchKey = KEYS[3]
|
|
16306
|
+
local queueId = ARGV[1]
|
|
16307
|
+
local tenantId = ARGV[2]
|
|
16308
|
+
|
|
16309
|
+
local count = redis.call('ZCARD', queueKey)
|
|
16310
|
+
if count == 0 then
|
|
16311
|
+
-- Queue is empty: remove from tenant queue index
|
|
16312
|
+
redis.call('ZREM', tenantQueueIndexKey, queueId)
|
|
16313
|
+
|
|
16314
|
+
-- Check if tenant has any queues left
|
|
16315
|
+
local tenantQueueCount = redis.call('ZCARD', tenantQueueIndexKey)
|
|
16316
|
+
if tenantQueueCount == 0 then
|
|
16317
|
+
-- No more queues: remove tenant from dispatch
|
|
16318
|
+
redis.call('ZREM', dispatchKey, tenantId)
|
|
16319
|
+
else
|
|
16320
|
+
-- Update dispatch score to tenant's new oldest
|
|
16321
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
16322
|
+
if #tenantOldest >= 2 then
|
|
16323
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
16324
|
+
end
|
|
16325
|
+
end
|
|
16326
|
+
return 1
|
|
15582
16327
|
else
|
|
15583
|
-
--
|
|
16328
|
+
-- Queue still has messages: update scores
|
|
15584
16329
|
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
15585
16330
|
if #oldest >= 2 then
|
|
15586
|
-
redis.call('ZADD',
|
|
16331
|
+
redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
|
|
16332
|
+
end
|
|
16333
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
16334
|
+
if #tenantOldest >= 2 then
|
|
16335
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
15587
16336
|
end
|
|
15588
16337
|
return 0
|
|
15589
16338
|
end
|
|
@@ -15616,6 +16365,7 @@ exports.NoRetry = NoRetry;
|
|
|
15616
16365
|
exports.NoopScheduler = NoopScheduler;
|
|
15617
16366
|
exports.RoundRobinScheduler = RoundRobinScheduler;
|
|
15618
16367
|
exports.SimpleQueue = SimpleQueue;
|
|
16368
|
+
exports.TenantDispatch = TenantDispatch;
|
|
15619
16369
|
exports.VisibilityManager = VisibilityManager;
|
|
15620
16370
|
exports.WeightedScheduler = WeightedScheduler;
|
|
15621
16371
|
exports.Worker = Worker;
|