@trigger.dev/redis-worker 4.7.2 → 4.7.4
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 +472 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -18
- package/dist/index.d.ts +163 -18
- package/dist/index.js +467 -60
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9634,6 +9634,39 @@ function createRedisClient(options, handlers) {
|
|
|
9634
9634
|
});
|
|
9635
9635
|
return client;
|
|
9636
9636
|
}
|
|
9637
|
+
/**
|
|
9638
|
+
* Cluster-mode client. `defaultOptions` go on the INNER per-node options, so a role swap gets the
|
|
9639
|
+
* same reconnect-and-retry treatment a single-node client already gets.
|
|
9640
|
+
*/
|
|
9641
|
+
function createRedisClusterClient(options, handlers) {
|
|
9642
|
+
const client = new import_built.Redis.Cluster(options.nodes, {
|
|
9643
|
+
...options.failFast && { enableOfflineQueue: false },
|
|
9644
|
+
...options.clusterOptions,
|
|
9645
|
+
redisOptions: {
|
|
9646
|
+
...defaultOptions,
|
|
9647
|
+
...options.failFast && { enableOfflineQueue: false },
|
|
9648
|
+
...options.redisOptions
|
|
9649
|
+
}
|
|
9650
|
+
});
|
|
9651
|
+
if (process.env.VITEST) {
|
|
9652
|
+
client.on("error", () => {});
|
|
9653
|
+
return client;
|
|
9654
|
+
}
|
|
9655
|
+
client.on("error", (error) => {
|
|
9656
|
+
if (handlers?.onError) handlers.onError(error);
|
|
9657
|
+
else logger.error(`Redis cluster client error:`, {
|
|
9658
|
+
error,
|
|
9659
|
+
keyPrefix: options.redisOptions?.keyPrefix
|
|
9660
|
+
});
|
|
9661
|
+
});
|
|
9662
|
+
client.on("node error", (error, node) => {
|
|
9663
|
+
logger.warn(`Redis cluster node error:`, {
|
|
9664
|
+
node,
|
|
9665
|
+
error: error.message
|
|
9666
|
+
});
|
|
9667
|
+
});
|
|
9668
|
+
return client;
|
|
9669
|
+
}
|
|
9637
9670
|
|
|
9638
9671
|
//#endregion
|
|
9639
9672
|
//#region ../../node_modules/.pnpm/nanoid@5.1.16/node_modules/nanoid/url-alphabet/index.js
|
|
@@ -9719,6 +9752,23 @@ var SimpleQueue = class {
|
|
|
9719
9752
|
throw e;
|
|
9720
9753
|
}
|
|
9721
9754
|
}
|
|
9755
|
+
/**
|
|
9756
|
+
* Re-enqueues an item for a retry, unless since it was dequeued a newer enqueue replaced it (its
|
|
9757
|
+
* stored deduplication key no longer matches) or it was removed by an ack or cancel. Returns
|
|
9758
|
+
* false when it skipped, so a failed attempt never overwrites the newer item or revives a removed
|
|
9759
|
+
* one. A newer enqueue starts its own retry budget: an item a handler re-enqueues before failing
|
|
9760
|
+
* restarts at attempt 0 rather than moving towards the DLQ.
|
|
9761
|
+
*/
|
|
9762
|
+
async requeue({ id, job, item, attempt, availableAt, visibilityTimeoutMs, deduplicationKey }) {
|
|
9763
|
+
const serializedItem = JSON.stringify({
|
|
9764
|
+
job,
|
|
9765
|
+
item,
|
|
9766
|
+
visibilityTimeoutMs,
|
|
9767
|
+
attempt,
|
|
9768
|
+
deduplicationKey: nanoid()
|
|
9769
|
+
});
|
|
9770
|
+
return await this.redis.requeueItem(`queue`, `items`, id, availableAt.getTime(), serializedItem, deduplicationKey ?? "") === 1;
|
|
9771
|
+
}
|
|
9722
9772
|
async enqueueOnce({ id, job, item, attempt, availableAt, visibilityTimeoutMs }) {
|
|
9723
9773
|
if (!id) throw new Error("enqueueOnce requires an id");
|
|
9724
9774
|
try {
|
|
@@ -9886,14 +9936,21 @@ var SimpleQueue = class {
|
|
|
9886
9936
|
deduplicationKey: item.deduplicationKey ?? void 0
|
|
9887
9937
|
};
|
|
9888
9938
|
}
|
|
9889
|
-
|
|
9939
|
+
/**
|
|
9940
|
+
* Moves an item to the dead letter queue. With a deduplication key, an item a newer enqueue has
|
|
9941
|
+
* replaced is left in the queue and false is returned.
|
|
9942
|
+
*/
|
|
9943
|
+
async moveToDeadLetterQueue(id, errorMessage, deduplicationKey) {
|
|
9890
9944
|
try {
|
|
9891
9945
|
this.logger.debug(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): moving item to DLQ`, {
|
|
9892
9946
|
queue: this.name,
|
|
9893
9947
|
id,
|
|
9894
9948
|
errorMessage
|
|
9895
9949
|
});
|
|
9896
|
-
|
|
9950
|
+
const result = await this.redis.moveToDeadLetterQueue(`queue`, `items`, `dlq`, `dlq:items`, id, errorMessage, deduplicationKey ?? "");
|
|
9951
|
+
if (result === 2) return false;
|
|
9952
|
+
if (result !== 1) throw new Error("Move to Dead Letter Queue operation failed");
|
|
9953
|
+
return true;
|
|
9897
9954
|
} catch (e) {
|
|
9898
9955
|
this.logger.error(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): error moving item to DLQ`, {
|
|
9899
9956
|
queue: this.name,
|
|
@@ -10079,6 +10136,35 @@ var SimpleQueue = class {
|
|
|
10079
10136
|
-- Remove from sorted set and hash
|
|
10080
10137
|
redis.call('ZREM', queueKey, id)
|
|
10081
10138
|
redis.call('HDEL', itemsKey, id)
|
|
10139
|
+
return 1
|
|
10140
|
+
`
|
|
10141
|
+
});
|
|
10142
|
+
this.redis.defineCommand("requeueItem", {
|
|
10143
|
+
numberOfKeys: 2,
|
|
10144
|
+
lua: `
|
|
10145
|
+
local queue = KEYS[1]
|
|
10146
|
+
local items = KEYS[2]
|
|
10147
|
+
local id = ARGV[1]
|
|
10148
|
+
local score = ARGV[2]
|
|
10149
|
+
local serializedItem = ARGV[3]
|
|
10150
|
+
local deduplicationKey = ARGV[4]
|
|
10151
|
+
|
|
10152
|
+
-- Leave the item alone if, while this attempt ran, it was removed (acked or cancelled)
|
|
10153
|
+
-- or a newer enqueue replaced it.
|
|
10154
|
+
if deduplicationKey ~= "" then
|
|
10155
|
+
local current = redis.call('HGET', items, id)
|
|
10156
|
+
if not current then
|
|
10157
|
+
return 2
|
|
10158
|
+
end
|
|
10159
|
+
local success, parsed = pcall(cjson.decode, current)
|
|
10160
|
+
if success and parsed.deduplicationKey and parsed.deduplicationKey ~= deduplicationKey then
|
|
10161
|
+
return 2
|
|
10162
|
+
end
|
|
10163
|
+
end
|
|
10164
|
+
|
|
10165
|
+
redis.call('ZADD', queue, score, id)
|
|
10166
|
+
redis.call('HSET', items, id, serializedItem)
|
|
10167
|
+
|
|
10082
10168
|
return 1
|
|
10083
10169
|
`
|
|
10084
10170
|
});
|
|
@@ -10091,6 +10177,7 @@ var SimpleQueue = class {
|
|
|
10091
10177
|
local dlqItems = KEYS[4]
|
|
10092
10178
|
local id = ARGV[1]
|
|
10093
10179
|
local errorMessage = ARGV[2]
|
|
10180
|
+
local deduplicationKey = ARGV[3]
|
|
10094
10181
|
|
|
10095
10182
|
local item = redis.call('HGET', items, id)
|
|
10096
10183
|
if not item then
|
|
@@ -10098,6 +10185,9 @@ var SimpleQueue = class {
|
|
|
10098
10185
|
end
|
|
10099
10186
|
|
|
10100
10187
|
local parsedItem = cjson.decode(item)
|
|
10188
|
+
if deduplicationKey ~= "" and parsedItem.deduplicationKey and parsedItem.deduplicationKey ~= deduplicationKey then
|
|
10189
|
+
return 2
|
|
10190
|
+
end
|
|
10101
10191
|
parsedItem.errorMessage = errorMessage
|
|
10102
10192
|
|
|
10103
10193
|
local time = redis.call('TIME')
|
|
@@ -11821,7 +11911,7 @@ var Worker = class Worker {
|
|
|
11821
11911
|
else if (errorLogLevel === "warn") this.logger.warn(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
|
|
11822
11912
|
else if (errorLogLevel === "info") this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
|
|
11823
11913
|
else this.logger.error(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
|
|
11824
|
-
await this.queue.moveToDeadLetterQueue(item.id, errorMessage);
|
|
11914
|
+
if (!await this.queue.moveToDeadLetterQueue(item.id, errorMessage, item.deduplicationKey)) this.#logSuperseded("dead-letter", item.id, jobType);
|
|
11825
11915
|
continue;
|
|
11826
11916
|
}
|
|
11827
11917
|
const retryDate = new Date(Date.now() + retryDelay);
|
|
@@ -11833,14 +11923,15 @@ var Worker = class Worker {
|
|
|
11833
11923
|
retryDelay,
|
|
11834
11924
|
attempt: newAttempt
|
|
11835
11925
|
});
|
|
11836
|
-
await this.queue.
|
|
11926
|
+
if (!await this.queue.requeue({
|
|
11837
11927
|
id: item.id,
|
|
11838
11928
|
job: item.job,
|
|
11839
11929
|
item: item.item,
|
|
11840
11930
|
availableAt: retryDate,
|
|
11841
11931
|
attempt: newAttempt,
|
|
11842
|
-
visibilityTimeoutMs: item.visibilityTimeoutMs
|
|
11843
|
-
|
|
11932
|
+
visibilityTimeoutMs: item.visibilityTimeoutMs,
|
|
11933
|
+
deduplicationKey: item.deduplicationKey
|
|
11934
|
+
})) this.#logSuperseded("retry", item.id, jobType);
|
|
11844
11935
|
} catch (requeueError) {
|
|
11845
11936
|
this.logger.error(`Worker failed to requeue batch item. It will be retried after the visibility timeout.`, {
|
|
11846
11937
|
name: this.options.name,
|
|
@@ -11855,6 +11946,13 @@ var Worker = class Worker {
|
|
|
11855
11946
|
/**
|
|
11856
11947
|
* Processes a single item.
|
|
11857
11948
|
*/
|
|
11949
|
+
#logSuperseded(action, id, job) {
|
|
11950
|
+
this.logger.info(`Worker skipped ${action} for an item replaced or removed while it ran`, {
|
|
11951
|
+
name: this.options.name,
|
|
11952
|
+
id,
|
|
11953
|
+
job
|
|
11954
|
+
});
|
|
11955
|
+
}
|
|
11858
11956
|
async processItem({ id, job, item, visibilityTimeoutMs, attempt, timestamp, deduplicationKey }, batchSize, workerId, limiter) {
|
|
11859
11957
|
const catalogItem = this.options.catalog[job];
|
|
11860
11958
|
const handler = this.jobs[job];
|
|
@@ -11931,7 +12029,7 @@ var Worker = class Worker {
|
|
|
11931
12029
|
...logAttributes,
|
|
11932
12030
|
attempt: newAttempt
|
|
11933
12031
|
});
|
|
11934
|
-
await this.queue.moveToDeadLetterQueue(id, errorMessage);
|
|
12032
|
+
if (!await this.queue.moveToDeadLetterQueue(id, errorMessage, deduplicationKey)) this.#logSuperseded("dead-letter", id, job);
|
|
11935
12033
|
if (catalogItem.cron) await this.rescheduleCronJob(job, catalogItem, item);
|
|
11936
12034
|
return;
|
|
11937
12035
|
}
|
|
@@ -11945,14 +12043,15 @@ var Worker = class Worker {
|
|
|
11945
12043
|
visibilityTimeoutMs,
|
|
11946
12044
|
attempt: newAttempt
|
|
11947
12045
|
});
|
|
11948
|
-
await this.queue.
|
|
12046
|
+
if (!await this.queue.requeue({
|
|
11949
12047
|
id,
|
|
11950
12048
|
job,
|
|
11951
12049
|
item,
|
|
11952
12050
|
availableAt: retryDate,
|
|
11953
12051
|
attempt: newAttempt,
|
|
11954
|
-
visibilityTimeoutMs
|
|
11955
|
-
|
|
12052
|
+
visibilityTimeoutMs,
|
|
12053
|
+
deduplicationKey
|
|
12054
|
+
})) this.#logSuperseded("retry", id, job);
|
|
11956
12055
|
} catch (requeueError) {
|
|
11957
12056
|
this.logger.error(`Worker failed to requeue item. It will be retried after the visibility timeout.`, {
|
|
11958
12057
|
name: this.options.name,
|
|
@@ -12111,6 +12210,33 @@ function isAbortError(error) {
|
|
|
12111
12210
|
return error instanceof Error && (error.name === "AbortError" || error.message === "AbortError");
|
|
12112
12211
|
}
|
|
12113
12212
|
|
|
12213
|
+
//#endregion
|
|
12214
|
+
//#region src/fair-queue/connection.ts
|
|
12215
|
+
function isClusterConnection(options) {
|
|
12216
|
+
return "cluster" in options && options.cluster !== void 0;
|
|
12217
|
+
}
|
|
12218
|
+
function createFairQueueRedisClient(options) {
|
|
12219
|
+
return isClusterConnection(options) ? createRedisClusterClient(options.cluster) : createRedisClient(options);
|
|
12220
|
+
}
|
|
12221
|
+
function keyPrefixOf(options) {
|
|
12222
|
+
return isClusterConnection(options) ? options.cluster.clusterOptions?.keyPrefix ?? "" : options.keyPrefix ?? "";
|
|
12223
|
+
}
|
|
12224
|
+
/**
|
|
12225
|
+
* Every key matching `pattern` (which must include the key prefix), page by page. A cluster doesn't
|
|
12226
|
+
* fan SCAN out to its nodes, so this scans each primary in turn.
|
|
12227
|
+
*/
|
|
12228
|
+
async function* scanKeys(client, pattern, count = 1e3) {
|
|
12229
|
+
const nodes = client instanceof import_built.Cluster ? client.nodes("master") : [client];
|
|
12230
|
+
for (const node of nodes) {
|
|
12231
|
+
let cursor = "0";
|
|
12232
|
+
do {
|
|
12233
|
+
const [next, keys] = await node.scan(cursor, "MATCH", pattern, "COUNT", count);
|
|
12234
|
+
cursor = next;
|
|
12235
|
+
if (keys.length > 0) yield keys;
|
|
12236
|
+
} while (cursor !== "0");
|
|
12237
|
+
}
|
|
12238
|
+
}
|
|
12239
|
+
|
|
12114
12240
|
//#endregion
|
|
12115
12241
|
//#region src/fair-queue/concurrency.ts
|
|
12116
12242
|
/**
|
|
@@ -12137,7 +12263,7 @@ var ConcurrencyManager = class {
|
|
|
12137
12263
|
logger;
|
|
12138
12264
|
constructor(options) {
|
|
12139
12265
|
this.options = options;
|
|
12140
|
-
this.redis =
|
|
12266
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
12141
12267
|
this.keys = options.keys;
|
|
12142
12268
|
this.groups = options.groups;
|
|
12143
12269
|
this.groupsByName = new Map(options.groups.map((g) => [g.name, g]));
|
|
@@ -12225,7 +12351,10 @@ var ConcurrencyManager = class {
|
|
|
12225
12351
|
* record can only be a leak; if the message is about to be re-claimed, reserve simply
|
|
12226
12352
|
* re-adds the member.
|
|
12227
12353
|
*
|
|
12228
|
-
*
|
|
12354
|
+
* With a tenant-sharded key producer, each shard's sets are checked against that shard's
|
|
12355
|
+
* in-flight data only, so the check stays in one cluster slot.
|
|
12356
|
+
*
|
|
12357
|
+
* @param inflightDataKeys - The in-flight data hash keys for every shard, in shard order. The sweep
|
|
12229
12358
|
* refuses to run when this is empty, since with nowhere to look for running messages
|
|
12230
12359
|
* every member would look orphaned and all concurrency accounting would be erased.
|
|
12231
12360
|
* @returns The message ids that were removed, and how many sets were checked
|
|
@@ -12238,46 +12367,49 @@ var ConcurrencyManager = class {
|
|
|
12238
12367
|
removed: []
|
|
12239
12368
|
};
|
|
12240
12369
|
}
|
|
12241
|
-
const keyPrefix = this.options.redis
|
|
12370
|
+
const keyPrefix = keyPrefixOf(this.options.redis);
|
|
12242
12371
|
let scannedSets = 0;
|
|
12243
12372
|
const removed = [];
|
|
12244
|
-
for (const
|
|
12245
|
-
const
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
if (page.length === 0) continue;
|
|
12259
|
-
sawMembers = true;
|
|
12260
|
-
for (let i = 0; i < page.length; i += SWEEP_MEMBER_CHUNK_SIZE) {
|
|
12261
|
-
const chunk = page.slice(i, i + SWEEP_MEMBER_CHUNK_SIZE);
|
|
12262
|
-
const removedIds = await this.redis.removeOrphanedConcurrencySlots(1 + inflightDataKeys.length, [key, ...inflightDataKeys], ...chunk);
|
|
12263
|
-
removed.push(...removedIds);
|
|
12264
|
-
}
|
|
12265
|
-
} while (memberCursor !== "0");
|
|
12266
|
-
if (sawMembers) scannedSets++;
|
|
12267
|
-
} catch (error) {
|
|
12268
|
-
this.logger.error("Failed to sweep concurrency set, skipping it", {
|
|
12269
|
-
key,
|
|
12270
|
-
error: error instanceof Error ? error.message : String(error)
|
|
12271
|
-
});
|
|
12373
|
+
for (const scope of this.#sweepScopes(inflightDataKeys)) for await (const foundKeys of scanKeys(this.redis, `${keyPrefix}${scope.pattern}`)) for (const fullKey of foundKeys) {
|
|
12374
|
+
const key = keyPrefix && fullKey.startsWith(keyPrefix) ? fullKey.slice(keyPrefix.length) : fullKey;
|
|
12375
|
+
try {
|
|
12376
|
+
let sawMembers = false;
|
|
12377
|
+
let memberCursor = "0";
|
|
12378
|
+
do {
|
|
12379
|
+
const [nextMemberCursor, page] = await this.redis.sscan(key, memberCursor, "COUNT", SWEEP_MEMBER_CHUNK_SIZE);
|
|
12380
|
+
memberCursor = nextMemberCursor;
|
|
12381
|
+
if (page.length === 0) continue;
|
|
12382
|
+
sawMembers = true;
|
|
12383
|
+
for (let i = 0; i < page.length; i += SWEEP_MEMBER_CHUNK_SIZE) {
|
|
12384
|
+
const chunk = page.slice(i, i + SWEEP_MEMBER_CHUNK_SIZE);
|
|
12385
|
+
const removedIds = await this.redis.removeOrphanedConcurrencySlots(1 + scope.inflightDataKeys.length, [key, ...scope.inflightDataKeys], ...chunk);
|
|
12386
|
+
removed.push(...removedIds);
|
|
12272
12387
|
}
|
|
12273
|
-
}
|
|
12274
|
-
|
|
12388
|
+
} while (memberCursor !== "0");
|
|
12389
|
+
if (sawMembers) scannedSets++;
|
|
12390
|
+
} catch (error) {
|
|
12391
|
+
this.logger.error("Failed to sweep concurrency set, skipping it", {
|
|
12392
|
+
key,
|
|
12393
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12394
|
+
});
|
|
12395
|
+
}
|
|
12275
12396
|
}
|
|
12276
12397
|
return {
|
|
12277
12398
|
scannedSets,
|
|
12278
12399
|
removed
|
|
12279
12400
|
};
|
|
12280
12401
|
}
|
|
12402
|
+
#sweepScopes(inflightDataKeys) {
|
|
12403
|
+
const keys = this.keys;
|
|
12404
|
+
if (keys.tenantShard && keys.concurrencyKeyPattern) return this.groups.flatMap((group) => inflightDataKeys.map((inflightDataKey, shardId) => ({
|
|
12405
|
+
pattern: keys.concurrencyKeyPattern(group.name, shardId),
|
|
12406
|
+
inflightDataKeys: [inflightDataKey]
|
|
12407
|
+
})));
|
|
12408
|
+
return this.groups.map((group) => ({
|
|
12409
|
+
pattern: keys.concurrencyKey(group.name, "*"),
|
|
12410
|
+
inflightDataKeys
|
|
12411
|
+
}));
|
|
12412
|
+
}
|
|
12281
12413
|
/**
|
|
12282
12414
|
* Get current concurrency for a specific group.
|
|
12283
12415
|
*/
|
|
@@ -12438,7 +12570,7 @@ var MasterQueue = class {
|
|
|
12438
12570
|
shardCount;
|
|
12439
12571
|
constructor(options) {
|
|
12440
12572
|
this.options = options;
|
|
12441
|
-
this.redis =
|
|
12573
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
12442
12574
|
this.keys = options.keys;
|
|
12443
12575
|
this.shardCount = Math.max(1, options.shardCount);
|
|
12444
12576
|
this.#registerCommands();
|
|
@@ -12558,6 +12690,8 @@ var MasterQueue = class {
|
|
|
12558
12690
|
* Provides better distribution than djb2 and minimal reshuffling when shard count changes.
|
|
12559
12691
|
*/
|
|
12560
12692
|
#hashToShard(queueId) {
|
|
12693
|
+
const keys = this.options.keys;
|
|
12694
|
+
if (keys.tenantShard) return keys.tenantShard(keys.extractTenantId(queueId));
|
|
12561
12695
|
return jumpHash(queueId, this.shardCount);
|
|
12562
12696
|
}
|
|
12563
12697
|
#registerCommands() {
|
|
@@ -12800,6 +12934,16 @@ var FairQueueTelemetry = class {
|
|
|
12800
12934
|
}
|
|
12801
12935
|
});
|
|
12802
12936
|
}
|
|
12937
|
+
if (callbacks.getOldestMessageAge && callbacks.shardCount) {
|
|
12938
|
+
const getOldestMessageAge = callbacks.getOldestMessageAge;
|
|
12939
|
+
const shardCount = callbacks.shardCount;
|
|
12940
|
+
this.metrics.oldestMessageAge.addCallback(async (observableResult) => {
|
|
12941
|
+
for (let shardId = 0; shardId < shardCount; shardId++) {
|
|
12942
|
+
const age = await getOldestMessageAge(shardId);
|
|
12943
|
+
observableResult.observe(age, { [FairQueueAttributes.SHARD_ID]: shardId.toString() });
|
|
12944
|
+
}
|
|
12945
|
+
});
|
|
12946
|
+
}
|
|
12803
12947
|
if (callbacks.getDLQLength && callbacks.observedTenants) {
|
|
12804
12948
|
const getDLQLength = callbacks.getDLQLength;
|
|
12805
12949
|
const tenants = callbacks.observedTenants;
|
|
@@ -12893,6 +13037,10 @@ var FairQueueTelemetry = class {
|
|
|
12893
13037
|
dlqLength: this.meter.createObservableGauge(`${this.name}.dlq.length`, {
|
|
12894
13038
|
description: "Number of messages in dead letter queue",
|
|
12895
13039
|
unit: "messages"
|
|
13040
|
+
}),
|
|
13041
|
+
oldestMessageAge: this.meter.createObservableGauge(`${this.name}.oldest_message_age`, {
|
|
13042
|
+
description: "Age of the oldest due message not yet completed in a shard, including claimed messages until their queue next completes or empties",
|
|
13043
|
+
unit: "ms"
|
|
12896
13044
|
})
|
|
12897
13045
|
};
|
|
12898
13046
|
}
|
|
@@ -13110,7 +13258,7 @@ var TenantDispatch = class {
|
|
|
13110
13258
|
shardCount;
|
|
13111
13259
|
constructor(options) {
|
|
13112
13260
|
this.options = options;
|
|
13113
|
-
this.redis =
|
|
13261
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
13114
13262
|
this.keys = options.keys;
|
|
13115
13263
|
this.shardCount = Math.max(1, options.shardCount);
|
|
13116
13264
|
}
|
|
@@ -13120,6 +13268,7 @@ var TenantDispatch = class {
|
|
|
13120
13268
|
* always maps to exactly one dispatch shard.
|
|
13121
13269
|
*/
|
|
13122
13270
|
getShardForTenant(tenantId) {
|
|
13271
|
+
if (this.options.keys.tenantShard) return this.options.keys.tenantShard(tenantId);
|
|
13123
13272
|
return jumpHash(tenantId, this.shardCount);
|
|
13124
13273
|
}
|
|
13125
13274
|
/**
|
|
@@ -13235,7 +13384,7 @@ var VisibilityManager = class {
|
|
|
13235
13384
|
logger;
|
|
13236
13385
|
constructor(options) {
|
|
13237
13386
|
this.options = options;
|
|
13238
|
-
this.redis =
|
|
13387
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
13239
13388
|
this.keys = options.keys;
|
|
13240
13389
|
this.shardCount = options.shardCount;
|
|
13241
13390
|
this.defaultTimeoutMs = options.defaultTimeoutMs;
|
|
@@ -13548,6 +13697,7 @@ var VisibilityManager = class {
|
|
|
13548
13697
|
* Must use same algorithm as MasterQueue for consistency.
|
|
13549
13698
|
*/
|
|
13550
13699
|
#getShardForQueue(queueId) {
|
|
13700
|
+
if (this.keys.tenantShard) return this.keys.tenantShard(this.keys.extractTenantId(queueId));
|
|
13551
13701
|
return jumpHash(queueId, this.shardCount);
|
|
13552
13702
|
}
|
|
13553
13703
|
#makeMember(messageId, queueId) {
|
|
@@ -13826,7 +13976,7 @@ var WorkerQueueManager = class {
|
|
|
13826
13976
|
logger;
|
|
13827
13977
|
constructor(options) {
|
|
13828
13978
|
this.options = options;
|
|
13829
|
-
this.redis =
|
|
13979
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
13830
13980
|
this.keys = options.keys;
|
|
13831
13981
|
this.logger = options.logger ?? {
|
|
13832
13982
|
debug: () => {},
|
|
@@ -13877,6 +14027,9 @@ var WorkerQueueManager = class {
|
|
|
13877
14027
|
*/
|
|
13878
14028
|
async blockingPop(workerQueueId, timeoutSeconds, signal) {
|
|
13879
14029
|
const workerQueueKey = this.keys.workerQueueKey(workerQueueId);
|
|
14030
|
+
if (signal?.aborted) return null;
|
|
14031
|
+
const readyMessageKey = await this.redis.lpop(workerQueueKey);
|
|
14032
|
+
if (readyMessageKey) return readyMessageKey;
|
|
13880
14033
|
const blockingClient = this.redis.duplicate();
|
|
13881
14034
|
const cleanup = signal ? () => {
|
|
13882
14035
|
blockingClient.disconnect();
|
|
@@ -14122,6 +14275,100 @@ var CallbackFairQueueKeyProducer = class extends DefaultFairQueueKeyProducer {
|
|
|
14122
14275
|
return this.groupExtractor(groupName, queueId);
|
|
14123
14276
|
}
|
|
14124
14277
|
};
|
|
14278
|
+
/**
|
|
14279
|
+
* Key producer for running a fair queue on a Redis cluster. Each tenant belongs to one shard,
|
|
14280
|
+
* `jumpHash(tenantId, shardCount)`, and every key of a shard carries the hash tag
|
|
14281
|
+
* `{prefix:shard}`, so a shard's queues, items, payloads, tenant indexes, dispatch index,
|
|
14282
|
+
* in-flight set and concurrency sets share a slot and every script stays single-slot.
|
|
14283
|
+
*
|
|
14284
|
+
* Concurrency groups must be keyed by tenant id. The shard count can't change without moving keys.
|
|
14285
|
+
*
|
|
14286
|
+
* Key structure (S = the tenant's shard):
|
|
14287
|
+
* - Dispatch index: {prefix:S}:dispatch
|
|
14288
|
+
* - Tenant queue index: {prefix:S}:tenantq:{tenantId}
|
|
14289
|
+
* - Queue: {prefix:S}:queue:{queueId}, items {prefix:S}:queue:{queueId}:items
|
|
14290
|
+
* - Payload: {prefix:S}:payload:{queueId}:{messageId}
|
|
14291
|
+
* - Payload bytes per tenant: {prefix:S}:payloadbytes:{tenantId}
|
|
14292
|
+
* - Concurrency: {prefix:S}:concurrency:{groupName}:{tenantId}
|
|
14293
|
+
* - In-flight: {prefix:S}:inflight, data {prefix:S}:inflight:data
|
|
14294
|
+
* - Dead letter queue: {prefix:S}:dlq:{tenantId}, data {prefix:S}:dlq:{tenantId}:data
|
|
14295
|
+
* - Legacy master queue: {prefix:S}:master
|
|
14296
|
+
* - Worker queue: {prefix}:worker:{consumerId} (its own slot)
|
|
14297
|
+
* - DRR deficits: {prefix}:drr:deficit (its own slot)
|
|
14298
|
+
*/
|
|
14299
|
+
var TenantShardedFairQueueKeyProducer = class {
|
|
14300
|
+
shardCount;
|
|
14301
|
+
prefix;
|
|
14302
|
+
tenantExtractor;
|
|
14303
|
+
groupExtractor;
|
|
14304
|
+
constructor(options) {
|
|
14305
|
+
if (!Number.isInteger(options.shardCount) || options.shardCount < 1) throw new Error(`shardCount must be a positive integer, got ${options.shardCount}`);
|
|
14306
|
+
this.prefix = options.prefix;
|
|
14307
|
+
this.shardCount = options.shardCount;
|
|
14308
|
+
this.tenantExtractor = options.extractTenantId;
|
|
14309
|
+
this.groupExtractor = options.extractGroupId;
|
|
14310
|
+
}
|
|
14311
|
+
tenantShard(tenantId) {
|
|
14312
|
+
return jumpHash(tenantId, this.shardCount);
|
|
14313
|
+
}
|
|
14314
|
+
masterQueueKey(shardId) {
|
|
14315
|
+
return `${this.#tag(shardId)}:master`;
|
|
14316
|
+
}
|
|
14317
|
+
queueKey(queueId) {
|
|
14318
|
+
return `${this.#queueTag(queueId)}:queue:${queueId}`;
|
|
14319
|
+
}
|
|
14320
|
+
queueItemsKey(queueId) {
|
|
14321
|
+
return `${this.#queueTag(queueId)}:queue:${queueId}:items`;
|
|
14322
|
+
}
|
|
14323
|
+
payloadKey(queueId, messageId) {
|
|
14324
|
+
return `${this.#queueTag(queueId)}:payload:${queueId}:${messageId}`;
|
|
14325
|
+
}
|
|
14326
|
+
tenantPayloadBytesKey(tenantId) {
|
|
14327
|
+
return `${this.#tag(this.tenantShard(tenantId))}:payloadbytes:${tenantId}`;
|
|
14328
|
+
}
|
|
14329
|
+
concurrencyKey(groupName, groupId) {
|
|
14330
|
+
return `${this.#tag(this.tenantShard(groupId))}:concurrency:${groupName}:${groupId}`;
|
|
14331
|
+
}
|
|
14332
|
+
concurrencyKeyPattern(groupName, shardId) {
|
|
14333
|
+
return `${this.#tag(shardId)}:concurrency:${groupName}:*`;
|
|
14334
|
+
}
|
|
14335
|
+
inflightKey(shardId) {
|
|
14336
|
+
return `${this.#tag(shardId)}:inflight`;
|
|
14337
|
+
}
|
|
14338
|
+
inflightDataKey(shardId) {
|
|
14339
|
+
return `${this.#tag(shardId)}:inflight:data`;
|
|
14340
|
+
}
|
|
14341
|
+
workerQueueKey(consumerId) {
|
|
14342
|
+
return `${this.prefix}:worker:${consumerId}`;
|
|
14343
|
+
}
|
|
14344
|
+
deadLetterQueueKey(tenantId) {
|
|
14345
|
+
return `${this.#tag(this.tenantShard(tenantId))}:dlq:${tenantId}`;
|
|
14346
|
+
}
|
|
14347
|
+
deadLetterQueueDataKey(tenantId) {
|
|
14348
|
+
return `${this.#tag(this.tenantShard(tenantId))}:dlq:${tenantId}:data`;
|
|
14349
|
+
}
|
|
14350
|
+
dispatchKey(shardId) {
|
|
14351
|
+
return `${this.#tag(shardId)}:dispatch`;
|
|
14352
|
+
}
|
|
14353
|
+
tenantQueueIndexKey(tenantId) {
|
|
14354
|
+
return `${this.#tag(this.tenantShard(tenantId))}:tenantq:${tenantId}`;
|
|
14355
|
+
}
|
|
14356
|
+
drrDeficitKey() {
|
|
14357
|
+
return `${this.prefix}:drr:deficit`;
|
|
14358
|
+
}
|
|
14359
|
+
extractTenantId(queueId) {
|
|
14360
|
+
return this.tenantExtractor(queueId);
|
|
14361
|
+
}
|
|
14362
|
+
extractGroupId(groupName, queueId) {
|
|
14363
|
+
return this.groupExtractor(groupName, queueId);
|
|
14364
|
+
}
|
|
14365
|
+
#queueTag(queueId) {
|
|
14366
|
+
return this.#tag(this.tenantShard(this.tenantExtractor(queueId)));
|
|
14367
|
+
}
|
|
14368
|
+
#tag(shardId) {
|
|
14369
|
+
return `{${this.prefix}:${shardId}}`;
|
|
14370
|
+
}
|
|
14371
|
+
};
|
|
14125
14372
|
|
|
14126
14373
|
//#endregion
|
|
14127
14374
|
//#region src/fair-queue/retry.ts
|
|
@@ -14338,7 +14585,7 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
14338
14585
|
constructor(config) {
|
|
14339
14586
|
super();
|
|
14340
14587
|
this.config = config;
|
|
14341
|
-
this.redis =
|
|
14588
|
+
this.redis = createFairQueueRedisClient(config.redis);
|
|
14342
14589
|
this.keys = config.keys;
|
|
14343
14590
|
this.quantum = config.quantum;
|
|
14344
14591
|
this.maxDeficit = config.maxDeficit;
|
|
@@ -14481,6 +14728,7 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
14481
14728
|
return result;
|
|
14482
14729
|
}
|
|
14483
14730
|
#deficitKey() {
|
|
14731
|
+
if (this.keys.drrDeficitKey) return this.keys.drrDeficitKey();
|
|
14484
14732
|
return `${this.keys.masterQueueKey(0).split(":")[0]}:drr:deficit`;
|
|
14485
14733
|
}
|
|
14486
14734
|
async #getTenantsFromDispatch(dispatchKey) {
|
|
@@ -14639,7 +14887,7 @@ var WeightedScheduler = class extends BaseScheduler {
|
|
|
14639
14887
|
constructor(config) {
|
|
14640
14888
|
super();
|
|
14641
14889
|
this.config = config;
|
|
14642
|
-
this.redis =
|
|
14890
|
+
this.redis = createFairQueueRedisClient(config.redis);
|
|
14643
14891
|
this.keys = config.keys;
|
|
14644
14892
|
this.rng = seedrandom(config.seed);
|
|
14645
14893
|
this.biases = config.biases ?? defaultBiases;
|
|
@@ -14894,7 +15142,7 @@ var RoundRobinScheduler = class extends BaseScheduler {
|
|
|
14894
15142
|
constructor(config) {
|
|
14895
15143
|
super();
|
|
14896
15144
|
this.config = config;
|
|
14897
|
-
this.redis =
|
|
15145
|
+
this.redis = createFairQueueRedisClient(config.redis);
|
|
14898
15146
|
this.keys = config.keys;
|
|
14899
15147
|
this.masterQueueLimit = config.masterQueueLimit ?? 1e3;
|
|
14900
15148
|
}
|
|
@@ -14966,6 +15214,17 @@ var RoundRobinScheduler = class extends BaseScheduler {
|
|
|
14966
15214
|
|
|
14967
15215
|
//#endregion
|
|
14968
15216
|
//#region src/fair-queue/index.ts
|
|
15217
|
+
/** An enqueue that would take a tenant past `tenantPayloadBytesLimit`. */
|
|
15218
|
+
var TenantQueueFullError = class extends Error {
|
|
15219
|
+
tenantId;
|
|
15220
|
+
limitBytes;
|
|
15221
|
+
constructor(tenantId, limitBytes) {
|
|
15222
|
+
super(`Tenant ${tenantId} is at its limit of ${limitBytes} queued payload bytes`);
|
|
15223
|
+
this.tenantId = tenantId;
|
|
15224
|
+
this.limitBytes = limitBytes;
|
|
15225
|
+
this.name = "TenantQueueFullError";
|
|
15226
|
+
}
|
|
15227
|
+
};
|
|
14969
15228
|
/**
|
|
14970
15229
|
* FairQueue is the main orchestrator for fair queue message routing.
|
|
14971
15230
|
*
|
|
@@ -14984,7 +15243,7 @@ var RoundRobinScheduler = class extends BaseScheduler {
|
|
|
14984
15243
|
*
|
|
14985
15244
|
* @typeParam TPayloadSchema - Zod schema for message payload validation
|
|
14986
15245
|
*/
|
|
14987
|
-
var FairQueue = class {
|
|
15246
|
+
var FairQueue = class FairQueue {
|
|
14988
15247
|
options;
|
|
14989
15248
|
redis;
|
|
14990
15249
|
keys;
|
|
@@ -15008,6 +15267,7 @@ var FairQueue = class {
|
|
|
15008
15267
|
reconcileIntervalMs;
|
|
15009
15268
|
workerQueueResolver;
|
|
15010
15269
|
batchClaimSize;
|
|
15270
|
+
separatePayloads;
|
|
15011
15271
|
cooloffEnabled;
|
|
15012
15272
|
cooloffThreshold;
|
|
15013
15273
|
cooloffPeriodMs;
|
|
@@ -15027,8 +15287,10 @@ var FairQueue = class {
|
|
|
15027
15287
|
tenantDispatch;
|
|
15028
15288
|
constructor(options) {
|
|
15029
15289
|
this.options = options;
|
|
15030
|
-
|
|
15290
|
+
FairQueue.#validateLayout(options);
|
|
15291
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
15031
15292
|
this.keys = options.keys;
|
|
15293
|
+
this.separatePayloads = options.separatePayloads ?? false;
|
|
15032
15294
|
this.scheduler = options.scheduler;
|
|
15033
15295
|
this.logger = options.logger ?? new Logger("FairQueue", "info");
|
|
15034
15296
|
this.abortController = new AbortController();
|
|
@@ -15036,7 +15298,7 @@ var FairQueue = class {
|
|
|
15036
15298
|
this.validateOnEnqueue = options.validateOnEnqueue ?? false;
|
|
15037
15299
|
this.retryStrategy = options.retry?.strategy;
|
|
15038
15300
|
this.deadLetterQueueEnabled = options.retry?.deadLetterQueue ?? true;
|
|
15039
|
-
this.shardCount = options.shardCount ?? 1;
|
|
15301
|
+
this.shardCount = options.keys.shardCount ?? options.shardCount ?? 1;
|
|
15040
15302
|
this.consumerCount = options.consumerCount ?? 1;
|
|
15041
15303
|
this.consumerIntervalMs = options.consumerIntervalMs ?? 100;
|
|
15042
15304
|
this.visibilityTimeoutMs = options.visibilityTimeoutMs ?? 3e4;
|
|
@@ -15128,6 +15390,9 @@ var FairQueue = class {
|
|
|
15128
15390
|
getDLQLength: async (tenantId) => {
|
|
15129
15391
|
return await this.getDeadLetterQueueLength(tenantId);
|
|
15130
15392
|
},
|
|
15393
|
+
getOldestMessageAge: async (shardId) => {
|
|
15394
|
+
return await this.getOldestMessageAge(shardId);
|
|
15395
|
+
},
|
|
15131
15396
|
shardCount: this.shardCount,
|
|
15132
15397
|
observedTenants: options?.observedTenants
|
|
15133
15398
|
});
|
|
@@ -15172,7 +15437,13 @@ var FairQueue = class {
|
|
|
15172
15437
|
}) : void 0,
|
|
15173
15438
|
metadata: options.metadata
|
|
15174
15439
|
};
|
|
15175
|
-
|
|
15440
|
+
if (this.separatePayloads) {
|
|
15441
|
+
const { payload, ...envelope } = storedMessage;
|
|
15442
|
+
const limit = options.ignorePayloadBytesLimit ? 0 : await this.options.tenantPayloadBytesLimit?.(options.tenantId) ?? 0;
|
|
15443
|
+
const result = await this.redis.enqueueMessageSeparatePayload(queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, this.keys.payloadKey(options.queueId, messageId), this.keys.inflightKey(this.masterQueue.getShardForQueue(options.queueId)), this.keys.tenantPayloadBytesKey(options.tenantId), options.queueId, messageId, timestamp.toString(), JSON.stringify(envelope), options.tenantId, JSON.stringify(payload), `${messageId}:${options.queueId}`, String(limit));
|
|
15444
|
+
if (result === -1) throw new TenantQueueFullError(options.tenantId, limit);
|
|
15445
|
+
span.setAttribute("enqueued", result === 1);
|
|
15446
|
+
} else await this.redis.enqueueMessageAtomicV2(queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, options.queueId, messageId, timestamp.toString(), JSON.stringify(storedMessage), options.tenantId);
|
|
15176
15447
|
span.setAttributes({
|
|
15177
15448
|
[FairQueueAttributes.QUEUE_ID]: options.queueId,
|
|
15178
15449
|
[FairQueueAttributes.TENANT_ID]: options.tenantId,
|
|
@@ -15196,6 +15467,7 @@ var FairQueue = class {
|
|
|
15196
15467
|
*/
|
|
15197
15468
|
async enqueueBatch(options) {
|
|
15198
15469
|
return this.telemetry.trace("enqueueBatch", async (span) => {
|
|
15470
|
+
if (this.separatePayloads) throw new Error("enqueueBatch isn't supported with separatePayloads");
|
|
15199
15471
|
const queueKey = this.keys.queueKey(options.queueId);
|
|
15200
15472
|
const queueItemsKey = this.keys.queueItemsKey(options.queueId);
|
|
15201
15473
|
const dispatchShardId = this.tenantDispatch.getShardForTenant(options.tenantId);
|
|
@@ -15450,6 +15722,17 @@ var FairQueue = class {
|
|
|
15450
15722
|
return dispatchCount + legacyCount;
|
|
15451
15723
|
}
|
|
15452
15724
|
/**
|
|
15725
|
+
* Get the age in ms of the oldest due message not yet completed in a shard, or 0 if none.
|
|
15726
|
+
* Claims don't refresh the index scores, so claimed messages count until their queue next completes or empties.
|
|
15727
|
+
*/
|
|
15728
|
+
async getOldestMessageAge(shardId) {
|
|
15729
|
+
const now = Date.now();
|
|
15730
|
+
const [[oldestTenant], [oldestLegacyQueue]] = await Promise.all([this.tenantDispatch.getTenantsFromShard(shardId, 1, now), this.masterQueue.getQueuesFromShard(shardId, 1, now)]);
|
|
15731
|
+
const oldestScore = Math.min(oldestTenant?.score ?? Infinity, oldestLegacyQueue?.score ?? Infinity);
|
|
15732
|
+
if (!Number.isFinite(oldestScore)) return 0;
|
|
15733
|
+
return Math.max(0, now - oldestScore);
|
|
15734
|
+
}
|
|
15735
|
+
/**
|
|
15453
15736
|
* Get total in-flight message count.
|
|
15454
15737
|
*/
|
|
15455
15738
|
async getTotalInflightCount() {
|
|
@@ -15712,10 +15995,22 @@ var FairQueue = class {
|
|
|
15712
15995
|
async getMessageData(messageId, queueId) {
|
|
15713
15996
|
const shardId = this.masterQueue.getShardForQueue(queueId);
|
|
15714
15997
|
const inflightDataKey = this.keys.inflightDataKey(shardId);
|
|
15715
|
-
const dataJson = await this.redis.hget(inflightDataKey, messageId);
|
|
15998
|
+
const [dataJson, payloadJson] = await Promise.all([this.redis.hget(inflightDataKey, messageId), this.separatePayloads ? this.redis.get(this.keys.payloadKey(queueId, messageId)) : Promise.resolve(null)]);
|
|
15716
15999
|
if (!dataJson) return null;
|
|
15717
16000
|
try {
|
|
15718
|
-
|
|
16001
|
+
const message = JSON.parse(dataJson);
|
|
16002
|
+
if (!this.separatePayloads) return message;
|
|
16003
|
+
if (payloadJson === null) {
|
|
16004
|
+
this.logger.error("In-flight message has no payload", {
|
|
16005
|
+
messageId,
|
|
16006
|
+
queueId
|
|
16007
|
+
});
|
|
16008
|
+
return null;
|
|
16009
|
+
}
|
|
16010
|
+
return {
|
|
16011
|
+
...message,
|
|
16012
|
+
payload: JSON.parse(payloadJson)
|
|
16013
|
+
};
|
|
15719
16014
|
} catch {
|
|
15720
16015
|
this.logger.error("Failed to parse message data", {
|
|
15721
16016
|
messageId,
|
|
@@ -15756,7 +16051,7 @@ var FairQueue = class {
|
|
|
15756
16051
|
metadata: storedMessage?.metadata ?? {}
|
|
15757
16052
|
};
|
|
15758
16053
|
await this.#releaseConcurrencySafely(descriptor, messageId, "completeMessage");
|
|
15759
|
-
await this
|
|
16054
|
+
await this.#completeInVisibility(messageId, queueId, descriptor.tenantId);
|
|
15760
16055
|
const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId);
|
|
15761
16056
|
if (queueEmpty) {
|
|
15762
16057
|
this.queueDescriptorCache.delete(queueId);
|
|
@@ -15906,9 +16201,16 @@ var FairQueue = class {
|
|
|
15906
16201
|
}
|
|
15907
16202
|
async #moveToDeadLetterQueue(storedMessage, errorMessage) {
|
|
15908
16203
|
if (!this.deadLetterQueueEnabled) {
|
|
15909
|
-
await this
|
|
16204
|
+
await this.#completeInVisibility(storedMessage.id, storedMessage.queueId, storedMessage.tenantId);
|
|
15910
16205
|
return;
|
|
15911
16206
|
}
|
|
16207
|
+
if (this.separatePayloads) {
|
|
16208
|
+
const payloadJson = await this.redis.get(this.keys.payloadKey(storedMessage.queueId, storedMessage.id));
|
|
16209
|
+
storedMessage = {
|
|
16210
|
+
...storedMessage,
|
|
16211
|
+
payload: payloadJson === null ? void 0 : JSON.parse(payloadJson)
|
|
16212
|
+
};
|
|
16213
|
+
}
|
|
15912
16214
|
const dlqKey = this.keys.deadLetterQueueKey(storedMessage.tenantId);
|
|
15913
16215
|
const dlqDataKey = this.keys.deadLetterQueueDataKey(storedMessage.tenantId);
|
|
15914
16216
|
this.masterQueue.getShardForQueue(storedMessage.queueId);
|
|
@@ -15922,7 +16224,7 @@ var FairQueue = class {
|
|
|
15922
16224
|
lastError: errorMessage,
|
|
15923
16225
|
originalTimestamp: storedMessage.timestamp
|
|
15924
16226
|
};
|
|
15925
|
-
await this
|
|
16227
|
+
await this.#completeInVisibility(storedMessage.id, storedMessage.queueId, storedMessage.tenantId);
|
|
15926
16228
|
const pipeline = this.redis.pipeline();
|
|
15927
16229
|
pipeline.zadd(dlqKey, dlqMessage.deadLetteredAt, storedMessage.id);
|
|
15928
16230
|
pipeline.hset(dlqDataKey, storedMessage.id, JSON.stringify(dlqMessage));
|
|
@@ -16068,6 +16370,40 @@ var FairQueue = class {
|
|
|
16068
16370
|
const removedFromDispatch = await this.redis.updateDispatchIndexes(queueKey, tenantQueueIndexKey, dispatchKey, queueId, tenantId);
|
|
16069
16371
|
return { queueEmpty: removedFromMaster === 1 || removedFromDispatch === 1 };
|
|
16070
16372
|
}
|
|
16373
|
+
/**
|
|
16374
|
+
* Remove a settled message from in-flight. With separate payloads this also deletes the payload
|
|
16375
|
+
* and takes its bytes off the tenant's count, in one script, so the count never drifts.
|
|
16376
|
+
*/
|
|
16377
|
+
async #completeInVisibility(messageId, queueId, tenantId) {
|
|
16378
|
+
if (!this.separatePayloads) {
|
|
16379
|
+
await this.visibilityManager.complete(messageId, queueId);
|
|
16380
|
+
return;
|
|
16381
|
+
}
|
|
16382
|
+
const shardId = this.masterQueue.getShardForQueue(queueId);
|
|
16383
|
+
await this.redis.completeSeparatePayload(this.keys.inflightKey(shardId), this.keys.inflightDataKey(shardId), this.keys.payloadKey(queueId, messageId), this.keys.tenantPayloadBytesKey(tenantId), `${messageId}:${queueId}`, messageId);
|
|
16384
|
+
}
|
|
16385
|
+
/** A tenant's queued and in-flight payload bytes, with `separatePayloads`. */
|
|
16386
|
+
async getTenantPayloadBytes(tenantId) {
|
|
16387
|
+
if (!this.separatePayloads) return 0;
|
|
16388
|
+
return Number(await this.redis.get(this.keys.tenantPayloadBytesKey(tenantId)) ?? 0);
|
|
16389
|
+
}
|
|
16390
|
+
/**
|
|
16391
|
+
* A cluster needs every script's keys in one slot, which only a tenant-sharded key producer
|
|
16392
|
+
* gives, and the concurrency reservation touches every group's set at once, so it allows one
|
|
16393
|
+
* group, keyed by tenant.
|
|
16394
|
+
*/
|
|
16395
|
+
static #validateLayout(options) {
|
|
16396
|
+
const keys = options.keys;
|
|
16397
|
+
if (options.separatePayloads && (!keys.payloadKey || !keys.tenantPayloadBytesKey)) throw new Error("separatePayloads needs a key producer with payloadKey and tenantPayloadBytesKey");
|
|
16398
|
+
if (keys.tenantShard) {
|
|
16399
|
+
if (!keys.shardCount) throw new Error("A key producer with tenantShard must set shardCount");
|
|
16400
|
+
if (options.shardCount !== void 0 && options.shardCount !== keys.shardCount) throw new Error(`shardCount ${options.shardCount} doesn't match the key producer's ${keys.shardCount}`);
|
|
16401
|
+
}
|
|
16402
|
+
if (isClusterConnection(options.redis)) {
|
|
16403
|
+
if (!keys.tenantShard) throw new Error("A cluster connection needs a tenant-sharded key producer");
|
|
16404
|
+
if ((options.concurrencyGroups?.length ?? 0) > 1) throw new Error("A cluster connection supports at most one concurrency group");
|
|
16405
|
+
}
|
|
16406
|
+
}
|
|
16071
16407
|
#createSchedulerContext() {
|
|
16072
16408
|
return {
|
|
16073
16409
|
getCurrentConcurrency: async (groupName, groupId) => {
|
|
@@ -16226,6 +16562,77 @@ if #tenantOldest >= 2 then
|
|
|
16226
16562
|
end
|
|
16227
16563
|
|
|
16228
16564
|
return (#ARGV - 2) / 3
|
|
16565
|
+
`
|
|
16566
|
+
});
|
|
16567
|
+
this.redis.defineCommand("enqueueMessageSeparatePayload", {
|
|
16568
|
+
numberOfKeys: 7,
|
|
16569
|
+
lua: `#!lua
|
|
16570
|
+
local queueKey = KEYS[1]
|
|
16571
|
+
local queueItemsKey = KEYS[2]
|
|
16572
|
+
local tenantQueueIndexKey = KEYS[3]
|
|
16573
|
+
local dispatchKey = KEYS[4]
|
|
16574
|
+
local payloadKey = KEYS[5]
|
|
16575
|
+
local inflightKey = KEYS[6]
|
|
16576
|
+
local bytesKey = KEYS[7]
|
|
16577
|
+
|
|
16578
|
+
local queueId = ARGV[1]
|
|
16579
|
+
local messageId = ARGV[2]
|
|
16580
|
+
local timestamp = tonumber(ARGV[3])
|
|
16581
|
+
local envelope = ARGV[4]
|
|
16582
|
+
local tenantId = ARGV[5]
|
|
16583
|
+
local payload = ARGV[6]
|
|
16584
|
+
local inflightMember = ARGV[7]
|
|
16585
|
+
local limit = tonumber(ARGV[8])
|
|
16586
|
+
|
|
16587
|
+
if redis.call('HEXISTS', queueItemsKey, messageId) == 1 then
|
|
16588
|
+
return 0
|
|
16589
|
+
end
|
|
16590
|
+
if redis.call('ZSCORE', inflightKey, inflightMember) then
|
|
16591
|
+
return 0
|
|
16592
|
+
end
|
|
16593
|
+
|
|
16594
|
+
local size = string.len(payload)
|
|
16595
|
+
if limit > 0 and tonumber(redis.call('GET', bytesKey) or '0') + size > limit then
|
|
16596
|
+
return -1
|
|
16597
|
+
end
|
|
16598
|
+
|
|
16599
|
+
redis.call('SET', payloadKey, payload)
|
|
16600
|
+
redis.call('INCRBY', bytesKey, size)
|
|
16601
|
+
redis.call('ZADD', queueKey, timestamp, messageId)
|
|
16602
|
+
redis.call('HSET', queueItemsKey, messageId, envelope)
|
|
16603
|
+
|
|
16604
|
+
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
16605
|
+
if #oldest >= 2 then
|
|
16606
|
+
redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
|
|
16607
|
+
end
|
|
16608
|
+
|
|
16609
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
16610
|
+
if #tenantOldest >= 2 then
|
|
16611
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
16612
|
+
end
|
|
16613
|
+
|
|
16614
|
+
return 1
|
|
16615
|
+
`
|
|
16616
|
+
});
|
|
16617
|
+
this.redis.defineCommand("completeSeparatePayload", {
|
|
16618
|
+
numberOfKeys: 4,
|
|
16619
|
+
lua: `
|
|
16620
|
+
local inflightKey = KEYS[1]
|
|
16621
|
+
local inflightDataKey = KEYS[2]
|
|
16622
|
+
local payloadKey = KEYS[3]
|
|
16623
|
+
local bytesKey = KEYS[4]
|
|
16624
|
+
|
|
16625
|
+
redis.call('ZREM', inflightKey, ARGV[1])
|
|
16626
|
+
redis.call('HDEL', inflightDataKey, ARGV[2])
|
|
16627
|
+
|
|
16628
|
+
local size = redis.call('STRLEN', payloadKey)
|
|
16629
|
+
if size > 0 then
|
|
16630
|
+
redis.call('DEL', payloadKey)
|
|
16631
|
+
if redis.call('DECRBY', bytesKey, size) <= 0 then
|
|
16632
|
+
redis.call('DEL', bytesKey)
|
|
16633
|
+
end
|
|
16634
|
+
end
|
|
16635
|
+
return size
|
|
16229
16636
|
`
|
|
16230
16637
|
});
|
|
16231
16638
|
this.redis.defineCommand("updateDispatchIndexes", {
|
|
@@ -17348,5 +17755,5 @@ var MollifierDrainer = class {
|
|
|
17348
17755
|
};
|
|
17349
17756
|
|
|
17350
17757
|
//#endregion
|
|
17351
|
-
export { BaseScheduler, BatchedSpanManager, BufferEntryError, BufferEntrySchema, BufferEntryStatus, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, MollifierBuffer, MollifierDrainer, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, TenantDispatch, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, defaultRetryOptions, deserialiseSnapshot, idempotencyLookupKeyFor, isAbortError, makeIdempotencyClaimKey, noopTelemetry, serialiseSnapshot };
|
|
17758
|
+
export { BaseScheduler, BatchedSpanManager, BufferEntryError, BufferEntrySchema, BufferEntryStatus, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, MollifierBuffer, MollifierDrainer, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, TenantDispatch, TenantQueueFullError, TenantShardedFairQueueKeyProducer, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, createFairQueueRedisClient, defaultRetryOptions, deserialiseSnapshot, idempotencyLookupKeyFor, isAbortError, isClusterConnection, keyPrefixOf, makeIdempotencyClaimKey, noopTelemetry, scanKeys, serialiseSnapshot };
|
|
17352
17759
|
//# sourceMappingURL=index.js.map
|