@trigger.dev/redis-worker 4.7.3 → 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.cjs
CHANGED
|
@@ -9644,6 +9644,39 @@ function createRedisClient(options, handlers) {
|
|
|
9644
9644
|
});
|
|
9645
9645
|
return client;
|
|
9646
9646
|
}
|
|
9647
|
+
/**
|
|
9648
|
+
* Cluster-mode client. `defaultOptions` go on the INNER per-node options, so a role swap gets the
|
|
9649
|
+
* same reconnect-and-retry treatment a single-node client already gets.
|
|
9650
|
+
*/
|
|
9651
|
+
function createRedisClusterClient(options, handlers) {
|
|
9652
|
+
const client = new import_built.Redis.Cluster(options.nodes, {
|
|
9653
|
+
...options.failFast && { enableOfflineQueue: false },
|
|
9654
|
+
...options.clusterOptions,
|
|
9655
|
+
redisOptions: {
|
|
9656
|
+
...defaultOptions,
|
|
9657
|
+
...options.failFast && { enableOfflineQueue: false },
|
|
9658
|
+
...options.redisOptions
|
|
9659
|
+
}
|
|
9660
|
+
});
|
|
9661
|
+
if (process.env.VITEST) {
|
|
9662
|
+
client.on("error", () => {});
|
|
9663
|
+
return client;
|
|
9664
|
+
}
|
|
9665
|
+
client.on("error", (error) => {
|
|
9666
|
+
if (handlers?.onError) handlers.onError(error);
|
|
9667
|
+
else logger.error(`Redis cluster client error:`, {
|
|
9668
|
+
error,
|
|
9669
|
+
keyPrefix: options.redisOptions?.keyPrefix
|
|
9670
|
+
});
|
|
9671
|
+
});
|
|
9672
|
+
client.on("node error", (error, node) => {
|
|
9673
|
+
logger.warn(`Redis cluster node error:`, {
|
|
9674
|
+
node,
|
|
9675
|
+
error: error.message
|
|
9676
|
+
});
|
|
9677
|
+
});
|
|
9678
|
+
return client;
|
|
9679
|
+
}
|
|
9647
9680
|
|
|
9648
9681
|
//#endregion
|
|
9649
9682
|
//#region ../../node_modules/.pnpm/nanoid@5.1.16/node_modules/nanoid/url-alphabet/index.js
|
|
@@ -9729,6 +9762,23 @@ var SimpleQueue = class {
|
|
|
9729
9762
|
throw e;
|
|
9730
9763
|
}
|
|
9731
9764
|
}
|
|
9765
|
+
/**
|
|
9766
|
+
* Re-enqueues an item for a retry, unless since it was dequeued a newer enqueue replaced it (its
|
|
9767
|
+
* stored deduplication key no longer matches) or it was removed by an ack or cancel. Returns
|
|
9768
|
+
* false when it skipped, so a failed attempt never overwrites the newer item or revives a removed
|
|
9769
|
+
* one. A newer enqueue starts its own retry budget: an item a handler re-enqueues before failing
|
|
9770
|
+
* restarts at attempt 0 rather than moving towards the DLQ.
|
|
9771
|
+
*/
|
|
9772
|
+
async requeue({ id, job, item, attempt, availableAt, visibilityTimeoutMs, deduplicationKey }) {
|
|
9773
|
+
const serializedItem = JSON.stringify({
|
|
9774
|
+
job,
|
|
9775
|
+
item,
|
|
9776
|
+
visibilityTimeoutMs,
|
|
9777
|
+
attempt,
|
|
9778
|
+
deduplicationKey: nanoid()
|
|
9779
|
+
});
|
|
9780
|
+
return await this.redis.requeueItem(`queue`, `items`, id, availableAt.getTime(), serializedItem, deduplicationKey ?? "") === 1;
|
|
9781
|
+
}
|
|
9732
9782
|
async enqueueOnce({ id, job, item, attempt, availableAt, visibilityTimeoutMs }) {
|
|
9733
9783
|
if (!id) throw new Error("enqueueOnce requires an id");
|
|
9734
9784
|
try {
|
|
@@ -9896,14 +9946,21 @@ var SimpleQueue = class {
|
|
|
9896
9946
|
deduplicationKey: item.deduplicationKey ?? void 0
|
|
9897
9947
|
};
|
|
9898
9948
|
}
|
|
9899
|
-
|
|
9949
|
+
/**
|
|
9950
|
+
* Moves an item to the dead letter queue. With a deduplication key, an item a newer enqueue has
|
|
9951
|
+
* replaced is left in the queue and false is returned.
|
|
9952
|
+
*/
|
|
9953
|
+
async moveToDeadLetterQueue(id, errorMessage, deduplicationKey) {
|
|
9900
9954
|
try {
|
|
9901
9955
|
this.logger.debug(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): moving item to DLQ`, {
|
|
9902
9956
|
queue: this.name,
|
|
9903
9957
|
id,
|
|
9904
9958
|
errorMessage
|
|
9905
9959
|
});
|
|
9906
|
-
|
|
9960
|
+
const result = await this.redis.moveToDeadLetterQueue(`queue`, `items`, `dlq`, `dlq:items`, id, errorMessage, deduplicationKey ?? "");
|
|
9961
|
+
if (result === 2) return false;
|
|
9962
|
+
if (result !== 1) throw new Error("Move to Dead Letter Queue operation failed");
|
|
9963
|
+
return true;
|
|
9907
9964
|
} catch (e) {
|
|
9908
9965
|
this.logger.error(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): error moving item to DLQ`, {
|
|
9909
9966
|
queue: this.name,
|
|
@@ -10089,6 +10146,35 @@ var SimpleQueue = class {
|
|
|
10089
10146
|
-- Remove from sorted set and hash
|
|
10090
10147
|
redis.call('ZREM', queueKey, id)
|
|
10091
10148
|
redis.call('HDEL', itemsKey, id)
|
|
10149
|
+
return 1
|
|
10150
|
+
`
|
|
10151
|
+
});
|
|
10152
|
+
this.redis.defineCommand("requeueItem", {
|
|
10153
|
+
numberOfKeys: 2,
|
|
10154
|
+
lua: `
|
|
10155
|
+
local queue = KEYS[1]
|
|
10156
|
+
local items = KEYS[2]
|
|
10157
|
+
local id = ARGV[1]
|
|
10158
|
+
local score = ARGV[2]
|
|
10159
|
+
local serializedItem = ARGV[3]
|
|
10160
|
+
local deduplicationKey = ARGV[4]
|
|
10161
|
+
|
|
10162
|
+
-- Leave the item alone if, while this attempt ran, it was removed (acked or cancelled)
|
|
10163
|
+
-- or a newer enqueue replaced it.
|
|
10164
|
+
if deduplicationKey ~= "" then
|
|
10165
|
+
local current = redis.call('HGET', items, id)
|
|
10166
|
+
if not current then
|
|
10167
|
+
return 2
|
|
10168
|
+
end
|
|
10169
|
+
local success, parsed = pcall(cjson.decode, current)
|
|
10170
|
+
if success and parsed.deduplicationKey and parsed.deduplicationKey ~= deduplicationKey then
|
|
10171
|
+
return 2
|
|
10172
|
+
end
|
|
10173
|
+
end
|
|
10174
|
+
|
|
10175
|
+
redis.call('ZADD', queue, score, id)
|
|
10176
|
+
redis.call('HSET', items, id, serializedItem)
|
|
10177
|
+
|
|
10092
10178
|
return 1
|
|
10093
10179
|
`
|
|
10094
10180
|
});
|
|
@@ -10101,6 +10187,7 @@ var SimpleQueue = class {
|
|
|
10101
10187
|
local dlqItems = KEYS[4]
|
|
10102
10188
|
local id = ARGV[1]
|
|
10103
10189
|
local errorMessage = ARGV[2]
|
|
10190
|
+
local deduplicationKey = ARGV[3]
|
|
10104
10191
|
|
|
10105
10192
|
local item = redis.call('HGET', items, id)
|
|
10106
10193
|
if not item then
|
|
@@ -10108,6 +10195,9 @@ var SimpleQueue = class {
|
|
|
10108
10195
|
end
|
|
10109
10196
|
|
|
10110
10197
|
local parsedItem = cjson.decode(item)
|
|
10198
|
+
if deduplicationKey ~= "" and parsedItem.deduplicationKey and parsedItem.deduplicationKey ~= deduplicationKey then
|
|
10199
|
+
return 2
|
|
10200
|
+
end
|
|
10111
10201
|
parsedItem.errorMessage = errorMessage
|
|
10112
10202
|
|
|
10113
10203
|
local time = redis.call('TIME')
|
|
@@ -11831,7 +11921,7 @@ var Worker = class Worker {
|
|
|
11831
11921
|
else if (errorLogLevel === "warn") this.logger.warn(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
|
|
11832
11922
|
else if (errorLogLevel === "info") this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
|
|
11833
11923
|
else this.logger.error(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
|
|
11834
|
-
await this.queue.moveToDeadLetterQueue(item.id, errorMessage);
|
|
11924
|
+
if (!await this.queue.moveToDeadLetterQueue(item.id, errorMessage, item.deduplicationKey)) this.#logSuperseded("dead-letter", item.id, jobType);
|
|
11835
11925
|
continue;
|
|
11836
11926
|
}
|
|
11837
11927
|
const retryDate = new Date(Date.now() + retryDelay);
|
|
@@ -11843,14 +11933,15 @@ var Worker = class Worker {
|
|
|
11843
11933
|
retryDelay,
|
|
11844
11934
|
attempt: newAttempt
|
|
11845
11935
|
});
|
|
11846
|
-
await this.queue.
|
|
11936
|
+
if (!await this.queue.requeue({
|
|
11847
11937
|
id: item.id,
|
|
11848
11938
|
job: item.job,
|
|
11849
11939
|
item: item.item,
|
|
11850
11940
|
availableAt: retryDate,
|
|
11851
11941
|
attempt: newAttempt,
|
|
11852
|
-
visibilityTimeoutMs: item.visibilityTimeoutMs
|
|
11853
|
-
|
|
11942
|
+
visibilityTimeoutMs: item.visibilityTimeoutMs,
|
|
11943
|
+
deduplicationKey: item.deduplicationKey
|
|
11944
|
+
})) this.#logSuperseded("retry", item.id, jobType);
|
|
11854
11945
|
} catch (requeueError) {
|
|
11855
11946
|
this.logger.error(`Worker failed to requeue batch item. It will be retried after the visibility timeout.`, {
|
|
11856
11947
|
name: this.options.name,
|
|
@@ -11865,6 +11956,13 @@ var Worker = class Worker {
|
|
|
11865
11956
|
/**
|
|
11866
11957
|
* Processes a single item.
|
|
11867
11958
|
*/
|
|
11959
|
+
#logSuperseded(action, id, job) {
|
|
11960
|
+
this.logger.info(`Worker skipped ${action} for an item replaced or removed while it ran`, {
|
|
11961
|
+
name: this.options.name,
|
|
11962
|
+
id,
|
|
11963
|
+
job
|
|
11964
|
+
});
|
|
11965
|
+
}
|
|
11868
11966
|
async processItem({ id, job, item, visibilityTimeoutMs, attempt, timestamp, deduplicationKey }, batchSize, workerId, limiter) {
|
|
11869
11967
|
const catalogItem = this.options.catalog[job];
|
|
11870
11968
|
const handler = this.jobs[job];
|
|
@@ -11941,7 +12039,7 @@ var Worker = class Worker {
|
|
|
11941
12039
|
...logAttributes,
|
|
11942
12040
|
attempt: newAttempt
|
|
11943
12041
|
});
|
|
11944
|
-
await this.queue.moveToDeadLetterQueue(id, errorMessage);
|
|
12042
|
+
if (!await this.queue.moveToDeadLetterQueue(id, errorMessage, deduplicationKey)) this.#logSuperseded("dead-letter", id, job);
|
|
11945
12043
|
if (catalogItem.cron) await this.rescheduleCronJob(job, catalogItem, item);
|
|
11946
12044
|
return;
|
|
11947
12045
|
}
|
|
@@ -11955,14 +12053,15 @@ var Worker = class Worker {
|
|
|
11955
12053
|
visibilityTimeoutMs,
|
|
11956
12054
|
attempt: newAttempt
|
|
11957
12055
|
});
|
|
11958
|
-
await this.queue.
|
|
12056
|
+
if (!await this.queue.requeue({
|
|
11959
12057
|
id,
|
|
11960
12058
|
job,
|
|
11961
12059
|
item,
|
|
11962
12060
|
availableAt: retryDate,
|
|
11963
12061
|
attempt: newAttempt,
|
|
11964
|
-
visibilityTimeoutMs
|
|
11965
|
-
|
|
12062
|
+
visibilityTimeoutMs,
|
|
12063
|
+
deduplicationKey
|
|
12064
|
+
})) this.#logSuperseded("retry", id, job);
|
|
11966
12065
|
} catch (requeueError) {
|
|
11967
12066
|
this.logger.error(`Worker failed to requeue item. It will be retried after the visibility timeout.`, {
|
|
11968
12067
|
name: this.options.name,
|
|
@@ -12121,6 +12220,33 @@ function isAbortError(error) {
|
|
|
12121
12220
|
return error instanceof Error && (error.name === "AbortError" || error.message === "AbortError");
|
|
12122
12221
|
}
|
|
12123
12222
|
|
|
12223
|
+
//#endregion
|
|
12224
|
+
//#region src/fair-queue/connection.ts
|
|
12225
|
+
function isClusterConnection(options) {
|
|
12226
|
+
return "cluster" in options && options.cluster !== void 0;
|
|
12227
|
+
}
|
|
12228
|
+
function createFairQueueRedisClient(options) {
|
|
12229
|
+
return isClusterConnection(options) ? createRedisClusterClient(options.cluster) : createRedisClient(options);
|
|
12230
|
+
}
|
|
12231
|
+
function keyPrefixOf(options) {
|
|
12232
|
+
return isClusterConnection(options) ? options.cluster.clusterOptions?.keyPrefix ?? "" : options.keyPrefix ?? "";
|
|
12233
|
+
}
|
|
12234
|
+
/**
|
|
12235
|
+
* Every key matching `pattern` (which must include the key prefix), page by page. A cluster doesn't
|
|
12236
|
+
* fan SCAN out to its nodes, so this scans each primary in turn.
|
|
12237
|
+
*/
|
|
12238
|
+
async function* scanKeys(client, pattern, count = 1e3) {
|
|
12239
|
+
const nodes = client instanceof import_built.Cluster ? client.nodes("master") : [client];
|
|
12240
|
+
for (const node of nodes) {
|
|
12241
|
+
let cursor = "0";
|
|
12242
|
+
do {
|
|
12243
|
+
const [next, keys] = await node.scan(cursor, "MATCH", pattern, "COUNT", count);
|
|
12244
|
+
cursor = next;
|
|
12245
|
+
if (keys.length > 0) yield keys;
|
|
12246
|
+
} while (cursor !== "0");
|
|
12247
|
+
}
|
|
12248
|
+
}
|
|
12249
|
+
|
|
12124
12250
|
//#endregion
|
|
12125
12251
|
//#region src/fair-queue/concurrency.ts
|
|
12126
12252
|
/**
|
|
@@ -12147,7 +12273,7 @@ var ConcurrencyManager = class {
|
|
|
12147
12273
|
logger;
|
|
12148
12274
|
constructor(options) {
|
|
12149
12275
|
this.options = options;
|
|
12150
|
-
this.redis =
|
|
12276
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
12151
12277
|
this.keys = options.keys;
|
|
12152
12278
|
this.groups = options.groups;
|
|
12153
12279
|
this.groupsByName = new Map(options.groups.map((g) => [g.name, g]));
|
|
@@ -12235,7 +12361,10 @@ var ConcurrencyManager = class {
|
|
|
12235
12361
|
* record can only be a leak; if the message is about to be re-claimed, reserve simply
|
|
12236
12362
|
* re-adds the member.
|
|
12237
12363
|
*
|
|
12238
|
-
*
|
|
12364
|
+
* With a tenant-sharded key producer, each shard's sets are checked against that shard's
|
|
12365
|
+
* in-flight data only, so the check stays in one cluster slot.
|
|
12366
|
+
*
|
|
12367
|
+
* @param inflightDataKeys - The in-flight data hash keys for every shard, in shard order. The sweep
|
|
12239
12368
|
* refuses to run when this is empty, since with nowhere to look for running messages
|
|
12240
12369
|
* every member would look orphaned and all concurrency accounting would be erased.
|
|
12241
12370
|
* @returns The message ids that were removed, and how many sets were checked
|
|
@@ -12248,46 +12377,49 @@ var ConcurrencyManager = class {
|
|
|
12248
12377
|
removed: []
|
|
12249
12378
|
};
|
|
12250
12379
|
}
|
|
12251
|
-
const keyPrefix = this.options.redis
|
|
12380
|
+
const keyPrefix = keyPrefixOf(this.options.redis);
|
|
12252
12381
|
let scannedSets = 0;
|
|
12253
12382
|
const removed = [];
|
|
12254
|
-
for (const
|
|
12255
|
-
const
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
if (page.length === 0) continue;
|
|
12269
|
-
sawMembers = true;
|
|
12270
|
-
for (let i = 0; i < page.length; i += SWEEP_MEMBER_CHUNK_SIZE) {
|
|
12271
|
-
const chunk = page.slice(i, i + SWEEP_MEMBER_CHUNK_SIZE);
|
|
12272
|
-
const removedIds = await this.redis.removeOrphanedConcurrencySlots(1 + inflightDataKeys.length, [key, ...inflightDataKeys], ...chunk);
|
|
12273
|
-
removed.push(...removedIds);
|
|
12274
|
-
}
|
|
12275
|
-
} while (memberCursor !== "0");
|
|
12276
|
-
if (sawMembers) scannedSets++;
|
|
12277
|
-
} catch (error) {
|
|
12278
|
-
this.logger.error("Failed to sweep concurrency set, skipping it", {
|
|
12279
|
-
key,
|
|
12280
|
-
error: error instanceof Error ? error.message : String(error)
|
|
12281
|
-
});
|
|
12383
|
+
for (const scope of this.#sweepScopes(inflightDataKeys)) for await (const foundKeys of scanKeys(this.redis, `${keyPrefix}${scope.pattern}`)) for (const fullKey of foundKeys) {
|
|
12384
|
+
const key = keyPrefix && fullKey.startsWith(keyPrefix) ? fullKey.slice(keyPrefix.length) : fullKey;
|
|
12385
|
+
try {
|
|
12386
|
+
let sawMembers = false;
|
|
12387
|
+
let memberCursor = "0";
|
|
12388
|
+
do {
|
|
12389
|
+
const [nextMemberCursor, page] = await this.redis.sscan(key, memberCursor, "COUNT", SWEEP_MEMBER_CHUNK_SIZE);
|
|
12390
|
+
memberCursor = nextMemberCursor;
|
|
12391
|
+
if (page.length === 0) continue;
|
|
12392
|
+
sawMembers = true;
|
|
12393
|
+
for (let i = 0; i < page.length; i += SWEEP_MEMBER_CHUNK_SIZE) {
|
|
12394
|
+
const chunk = page.slice(i, i + SWEEP_MEMBER_CHUNK_SIZE);
|
|
12395
|
+
const removedIds = await this.redis.removeOrphanedConcurrencySlots(1 + scope.inflightDataKeys.length, [key, ...scope.inflightDataKeys], ...chunk);
|
|
12396
|
+
removed.push(...removedIds);
|
|
12282
12397
|
}
|
|
12283
|
-
}
|
|
12284
|
-
|
|
12398
|
+
} while (memberCursor !== "0");
|
|
12399
|
+
if (sawMembers) scannedSets++;
|
|
12400
|
+
} catch (error) {
|
|
12401
|
+
this.logger.error("Failed to sweep concurrency set, skipping it", {
|
|
12402
|
+
key,
|
|
12403
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12404
|
+
});
|
|
12405
|
+
}
|
|
12285
12406
|
}
|
|
12286
12407
|
return {
|
|
12287
12408
|
scannedSets,
|
|
12288
12409
|
removed
|
|
12289
12410
|
};
|
|
12290
12411
|
}
|
|
12412
|
+
#sweepScopes(inflightDataKeys) {
|
|
12413
|
+
const keys = this.keys;
|
|
12414
|
+
if (keys.tenantShard && keys.concurrencyKeyPattern) return this.groups.flatMap((group) => inflightDataKeys.map((inflightDataKey, shardId) => ({
|
|
12415
|
+
pattern: keys.concurrencyKeyPattern(group.name, shardId),
|
|
12416
|
+
inflightDataKeys: [inflightDataKey]
|
|
12417
|
+
})));
|
|
12418
|
+
return this.groups.map((group) => ({
|
|
12419
|
+
pattern: keys.concurrencyKey(group.name, "*"),
|
|
12420
|
+
inflightDataKeys
|
|
12421
|
+
}));
|
|
12422
|
+
}
|
|
12291
12423
|
/**
|
|
12292
12424
|
* Get current concurrency for a specific group.
|
|
12293
12425
|
*/
|
|
@@ -12448,7 +12580,7 @@ var MasterQueue = class {
|
|
|
12448
12580
|
shardCount;
|
|
12449
12581
|
constructor(options) {
|
|
12450
12582
|
this.options = options;
|
|
12451
|
-
this.redis =
|
|
12583
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
12452
12584
|
this.keys = options.keys;
|
|
12453
12585
|
this.shardCount = Math.max(1, options.shardCount);
|
|
12454
12586
|
this.#registerCommands();
|
|
@@ -12568,6 +12700,8 @@ var MasterQueue = class {
|
|
|
12568
12700
|
* Provides better distribution than djb2 and minimal reshuffling when shard count changes.
|
|
12569
12701
|
*/
|
|
12570
12702
|
#hashToShard(queueId) {
|
|
12703
|
+
const keys = this.options.keys;
|
|
12704
|
+
if (keys.tenantShard) return keys.tenantShard(keys.extractTenantId(queueId));
|
|
12571
12705
|
return (0, _trigger_dev_core_v3_serverOnly.jumpHash)(queueId, this.shardCount);
|
|
12572
12706
|
}
|
|
12573
12707
|
#registerCommands() {
|
|
@@ -12810,6 +12944,16 @@ var FairQueueTelemetry = class {
|
|
|
12810
12944
|
}
|
|
12811
12945
|
});
|
|
12812
12946
|
}
|
|
12947
|
+
if (callbacks.getOldestMessageAge && callbacks.shardCount) {
|
|
12948
|
+
const getOldestMessageAge = callbacks.getOldestMessageAge;
|
|
12949
|
+
const shardCount = callbacks.shardCount;
|
|
12950
|
+
this.metrics.oldestMessageAge.addCallback(async (observableResult) => {
|
|
12951
|
+
for (let shardId = 0; shardId < shardCount; shardId++) {
|
|
12952
|
+
const age = await getOldestMessageAge(shardId);
|
|
12953
|
+
observableResult.observe(age, { [FairQueueAttributes.SHARD_ID]: shardId.toString() });
|
|
12954
|
+
}
|
|
12955
|
+
});
|
|
12956
|
+
}
|
|
12813
12957
|
if (callbacks.getDLQLength && callbacks.observedTenants) {
|
|
12814
12958
|
const getDLQLength = callbacks.getDLQLength;
|
|
12815
12959
|
const tenants = callbacks.observedTenants;
|
|
@@ -12903,6 +13047,10 @@ var FairQueueTelemetry = class {
|
|
|
12903
13047
|
dlqLength: this.meter.createObservableGauge(`${this.name}.dlq.length`, {
|
|
12904
13048
|
description: "Number of messages in dead letter queue",
|
|
12905
13049
|
unit: "messages"
|
|
13050
|
+
}),
|
|
13051
|
+
oldestMessageAge: this.meter.createObservableGauge(`${this.name}.oldest_message_age`, {
|
|
13052
|
+
description: "Age of the oldest due message not yet completed in a shard, including claimed messages until their queue next completes or empties",
|
|
13053
|
+
unit: "ms"
|
|
12906
13054
|
})
|
|
12907
13055
|
};
|
|
12908
13056
|
}
|
|
@@ -13120,7 +13268,7 @@ var TenantDispatch = class {
|
|
|
13120
13268
|
shardCount;
|
|
13121
13269
|
constructor(options) {
|
|
13122
13270
|
this.options = options;
|
|
13123
|
-
this.redis =
|
|
13271
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
13124
13272
|
this.keys = options.keys;
|
|
13125
13273
|
this.shardCount = Math.max(1, options.shardCount);
|
|
13126
13274
|
}
|
|
@@ -13130,6 +13278,7 @@ var TenantDispatch = class {
|
|
|
13130
13278
|
* always maps to exactly one dispatch shard.
|
|
13131
13279
|
*/
|
|
13132
13280
|
getShardForTenant(tenantId) {
|
|
13281
|
+
if (this.options.keys.tenantShard) return this.options.keys.tenantShard(tenantId);
|
|
13133
13282
|
return (0, _trigger_dev_core_v3_serverOnly.jumpHash)(tenantId, this.shardCount);
|
|
13134
13283
|
}
|
|
13135
13284
|
/**
|
|
@@ -13245,7 +13394,7 @@ var VisibilityManager = class {
|
|
|
13245
13394
|
logger;
|
|
13246
13395
|
constructor(options) {
|
|
13247
13396
|
this.options = options;
|
|
13248
|
-
this.redis =
|
|
13397
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
13249
13398
|
this.keys = options.keys;
|
|
13250
13399
|
this.shardCount = options.shardCount;
|
|
13251
13400
|
this.defaultTimeoutMs = options.defaultTimeoutMs;
|
|
@@ -13558,6 +13707,7 @@ var VisibilityManager = class {
|
|
|
13558
13707
|
* Must use same algorithm as MasterQueue for consistency.
|
|
13559
13708
|
*/
|
|
13560
13709
|
#getShardForQueue(queueId) {
|
|
13710
|
+
if (this.keys.tenantShard) return this.keys.tenantShard(this.keys.extractTenantId(queueId));
|
|
13561
13711
|
return (0, _trigger_dev_core_v3_serverOnly.jumpHash)(queueId, this.shardCount);
|
|
13562
13712
|
}
|
|
13563
13713
|
#makeMember(messageId, queueId) {
|
|
@@ -13836,7 +13986,7 @@ var WorkerQueueManager = class {
|
|
|
13836
13986
|
logger;
|
|
13837
13987
|
constructor(options) {
|
|
13838
13988
|
this.options = options;
|
|
13839
|
-
this.redis =
|
|
13989
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
13840
13990
|
this.keys = options.keys;
|
|
13841
13991
|
this.logger = options.logger ?? {
|
|
13842
13992
|
debug: () => {},
|
|
@@ -13887,6 +14037,9 @@ var WorkerQueueManager = class {
|
|
|
13887
14037
|
*/
|
|
13888
14038
|
async blockingPop(workerQueueId, timeoutSeconds, signal) {
|
|
13889
14039
|
const workerQueueKey = this.keys.workerQueueKey(workerQueueId);
|
|
14040
|
+
if (signal?.aborted) return null;
|
|
14041
|
+
const readyMessageKey = await this.redis.lpop(workerQueueKey);
|
|
14042
|
+
if (readyMessageKey) return readyMessageKey;
|
|
13890
14043
|
const blockingClient = this.redis.duplicate();
|
|
13891
14044
|
const cleanup = signal ? () => {
|
|
13892
14045
|
blockingClient.disconnect();
|
|
@@ -14132,6 +14285,100 @@ var CallbackFairQueueKeyProducer = class extends DefaultFairQueueKeyProducer {
|
|
|
14132
14285
|
return this.groupExtractor(groupName, queueId);
|
|
14133
14286
|
}
|
|
14134
14287
|
};
|
|
14288
|
+
/**
|
|
14289
|
+
* Key producer for running a fair queue on a Redis cluster. Each tenant belongs to one shard,
|
|
14290
|
+
* `jumpHash(tenantId, shardCount)`, and every key of a shard carries the hash tag
|
|
14291
|
+
* `{prefix:shard}`, so a shard's queues, items, payloads, tenant indexes, dispatch index,
|
|
14292
|
+
* in-flight set and concurrency sets share a slot and every script stays single-slot.
|
|
14293
|
+
*
|
|
14294
|
+
* Concurrency groups must be keyed by tenant id. The shard count can't change without moving keys.
|
|
14295
|
+
*
|
|
14296
|
+
* Key structure (S = the tenant's shard):
|
|
14297
|
+
* - Dispatch index: {prefix:S}:dispatch
|
|
14298
|
+
* - Tenant queue index: {prefix:S}:tenantq:{tenantId}
|
|
14299
|
+
* - Queue: {prefix:S}:queue:{queueId}, items {prefix:S}:queue:{queueId}:items
|
|
14300
|
+
* - Payload: {prefix:S}:payload:{queueId}:{messageId}
|
|
14301
|
+
* - Payload bytes per tenant: {prefix:S}:payloadbytes:{tenantId}
|
|
14302
|
+
* - Concurrency: {prefix:S}:concurrency:{groupName}:{tenantId}
|
|
14303
|
+
* - In-flight: {prefix:S}:inflight, data {prefix:S}:inflight:data
|
|
14304
|
+
* - Dead letter queue: {prefix:S}:dlq:{tenantId}, data {prefix:S}:dlq:{tenantId}:data
|
|
14305
|
+
* - Legacy master queue: {prefix:S}:master
|
|
14306
|
+
* - Worker queue: {prefix}:worker:{consumerId} (its own slot)
|
|
14307
|
+
* - DRR deficits: {prefix}:drr:deficit (its own slot)
|
|
14308
|
+
*/
|
|
14309
|
+
var TenantShardedFairQueueKeyProducer = class {
|
|
14310
|
+
shardCount;
|
|
14311
|
+
prefix;
|
|
14312
|
+
tenantExtractor;
|
|
14313
|
+
groupExtractor;
|
|
14314
|
+
constructor(options) {
|
|
14315
|
+
if (!Number.isInteger(options.shardCount) || options.shardCount < 1) throw new Error(`shardCount must be a positive integer, got ${options.shardCount}`);
|
|
14316
|
+
this.prefix = options.prefix;
|
|
14317
|
+
this.shardCount = options.shardCount;
|
|
14318
|
+
this.tenantExtractor = options.extractTenantId;
|
|
14319
|
+
this.groupExtractor = options.extractGroupId;
|
|
14320
|
+
}
|
|
14321
|
+
tenantShard(tenantId) {
|
|
14322
|
+
return (0, _trigger_dev_core_v3_serverOnly.jumpHash)(tenantId, this.shardCount);
|
|
14323
|
+
}
|
|
14324
|
+
masterQueueKey(shardId) {
|
|
14325
|
+
return `${this.#tag(shardId)}:master`;
|
|
14326
|
+
}
|
|
14327
|
+
queueKey(queueId) {
|
|
14328
|
+
return `${this.#queueTag(queueId)}:queue:${queueId}`;
|
|
14329
|
+
}
|
|
14330
|
+
queueItemsKey(queueId) {
|
|
14331
|
+
return `${this.#queueTag(queueId)}:queue:${queueId}:items`;
|
|
14332
|
+
}
|
|
14333
|
+
payloadKey(queueId, messageId) {
|
|
14334
|
+
return `${this.#queueTag(queueId)}:payload:${queueId}:${messageId}`;
|
|
14335
|
+
}
|
|
14336
|
+
tenantPayloadBytesKey(tenantId) {
|
|
14337
|
+
return `${this.#tag(this.tenantShard(tenantId))}:payloadbytes:${tenantId}`;
|
|
14338
|
+
}
|
|
14339
|
+
concurrencyKey(groupName, groupId) {
|
|
14340
|
+
return `${this.#tag(this.tenantShard(groupId))}:concurrency:${groupName}:${groupId}`;
|
|
14341
|
+
}
|
|
14342
|
+
concurrencyKeyPattern(groupName, shardId) {
|
|
14343
|
+
return `${this.#tag(shardId)}:concurrency:${groupName}:*`;
|
|
14344
|
+
}
|
|
14345
|
+
inflightKey(shardId) {
|
|
14346
|
+
return `${this.#tag(shardId)}:inflight`;
|
|
14347
|
+
}
|
|
14348
|
+
inflightDataKey(shardId) {
|
|
14349
|
+
return `${this.#tag(shardId)}:inflight:data`;
|
|
14350
|
+
}
|
|
14351
|
+
workerQueueKey(consumerId) {
|
|
14352
|
+
return `${this.prefix}:worker:${consumerId}`;
|
|
14353
|
+
}
|
|
14354
|
+
deadLetterQueueKey(tenantId) {
|
|
14355
|
+
return `${this.#tag(this.tenantShard(tenantId))}:dlq:${tenantId}`;
|
|
14356
|
+
}
|
|
14357
|
+
deadLetterQueueDataKey(tenantId) {
|
|
14358
|
+
return `${this.#tag(this.tenantShard(tenantId))}:dlq:${tenantId}:data`;
|
|
14359
|
+
}
|
|
14360
|
+
dispatchKey(shardId) {
|
|
14361
|
+
return `${this.#tag(shardId)}:dispatch`;
|
|
14362
|
+
}
|
|
14363
|
+
tenantQueueIndexKey(tenantId) {
|
|
14364
|
+
return `${this.#tag(this.tenantShard(tenantId))}:tenantq:${tenantId}`;
|
|
14365
|
+
}
|
|
14366
|
+
drrDeficitKey() {
|
|
14367
|
+
return `${this.prefix}:drr:deficit`;
|
|
14368
|
+
}
|
|
14369
|
+
extractTenantId(queueId) {
|
|
14370
|
+
return this.tenantExtractor(queueId);
|
|
14371
|
+
}
|
|
14372
|
+
extractGroupId(groupName, queueId) {
|
|
14373
|
+
return this.groupExtractor(groupName, queueId);
|
|
14374
|
+
}
|
|
14375
|
+
#queueTag(queueId) {
|
|
14376
|
+
return this.#tag(this.tenantShard(this.tenantExtractor(queueId)));
|
|
14377
|
+
}
|
|
14378
|
+
#tag(shardId) {
|
|
14379
|
+
return `{${this.prefix}:${shardId}}`;
|
|
14380
|
+
}
|
|
14381
|
+
};
|
|
14135
14382
|
|
|
14136
14383
|
//#endregion
|
|
14137
14384
|
//#region src/fair-queue/retry.ts
|
|
@@ -14348,7 +14595,7 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
14348
14595
|
constructor(config) {
|
|
14349
14596
|
super();
|
|
14350
14597
|
this.config = config;
|
|
14351
|
-
this.redis =
|
|
14598
|
+
this.redis = createFairQueueRedisClient(config.redis);
|
|
14352
14599
|
this.keys = config.keys;
|
|
14353
14600
|
this.quantum = config.quantum;
|
|
14354
14601
|
this.maxDeficit = config.maxDeficit;
|
|
@@ -14491,6 +14738,7 @@ var DRRScheduler = class extends BaseScheduler {
|
|
|
14491
14738
|
return result;
|
|
14492
14739
|
}
|
|
14493
14740
|
#deficitKey() {
|
|
14741
|
+
if (this.keys.drrDeficitKey) return this.keys.drrDeficitKey();
|
|
14494
14742
|
return `${this.keys.masterQueueKey(0).split(":")[0]}:drr:deficit`;
|
|
14495
14743
|
}
|
|
14496
14744
|
async #getTenantsFromDispatch(dispatchKey) {
|
|
@@ -14649,7 +14897,7 @@ var WeightedScheduler = class extends BaseScheduler {
|
|
|
14649
14897
|
constructor(config) {
|
|
14650
14898
|
super();
|
|
14651
14899
|
this.config = config;
|
|
14652
|
-
this.redis =
|
|
14900
|
+
this.redis = createFairQueueRedisClient(config.redis);
|
|
14653
14901
|
this.keys = config.keys;
|
|
14654
14902
|
this.rng = (0, seedrandom.default)(config.seed);
|
|
14655
14903
|
this.biases = config.biases ?? defaultBiases;
|
|
@@ -14904,7 +15152,7 @@ var RoundRobinScheduler = class extends BaseScheduler {
|
|
|
14904
15152
|
constructor(config) {
|
|
14905
15153
|
super();
|
|
14906
15154
|
this.config = config;
|
|
14907
|
-
this.redis =
|
|
15155
|
+
this.redis = createFairQueueRedisClient(config.redis);
|
|
14908
15156
|
this.keys = config.keys;
|
|
14909
15157
|
this.masterQueueLimit = config.masterQueueLimit ?? 1e3;
|
|
14910
15158
|
}
|
|
@@ -14976,6 +15224,17 @@ var RoundRobinScheduler = class extends BaseScheduler {
|
|
|
14976
15224
|
|
|
14977
15225
|
//#endregion
|
|
14978
15226
|
//#region src/fair-queue/index.ts
|
|
15227
|
+
/** An enqueue that would take a tenant past `tenantPayloadBytesLimit`. */
|
|
15228
|
+
var TenantQueueFullError = class extends Error {
|
|
15229
|
+
tenantId;
|
|
15230
|
+
limitBytes;
|
|
15231
|
+
constructor(tenantId, limitBytes) {
|
|
15232
|
+
super(`Tenant ${tenantId} is at its limit of ${limitBytes} queued payload bytes`);
|
|
15233
|
+
this.tenantId = tenantId;
|
|
15234
|
+
this.limitBytes = limitBytes;
|
|
15235
|
+
this.name = "TenantQueueFullError";
|
|
15236
|
+
}
|
|
15237
|
+
};
|
|
14979
15238
|
/**
|
|
14980
15239
|
* FairQueue is the main orchestrator for fair queue message routing.
|
|
14981
15240
|
*
|
|
@@ -14994,7 +15253,7 @@ var RoundRobinScheduler = class extends BaseScheduler {
|
|
|
14994
15253
|
*
|
|
14995
15254
|
* @typeParam TPayloadSchema - Zod schema for message payload validation
|
|
14996
15255
|
*/
|
|
14997
|
-
var FairQueue = class {
|
|
15256
|
+
var FairQueue = class FairQueue {
|
|
14998
15257
|
options;
|
|
14999
15258
|
redis;
|
|
15000
15259
|
keys;
|
|
@@ -15018,6 +15277,7 @@ var FairQueue = class {
|
|
|
15018
15277
|
reconcileIntervalMs;
|
|
15019
15278
|
workerQueueResolver;
|
|
15020
15279
|
batchClaimSize;
|
|
15280
|
+
separatePayloads;
|
|
15021
15281
|
cooloffEnabled;
|
|
15022
15282
|
cooloffThreshold;
|
|
15023
15283
|
cooloffPeriodMs;
|
|
@@ -15037,8 +15297,10 @@ var FairQueue = class {
|
|
|
15037
15297
|
tenantDispatch;
|
|
15038
15298
|
constructor(options) {
|
|
15039
15299
|
this.options = options;
|
|
15040
|
-
|
|
15300
|
+
FairQueue.#validateLayout(options);
|
|
15301
|
+
this.redis = createFairQueueRedisClient(options.redis);
|
|
15041
15302
|
this.keys = options.keys;
|
|
15303
|
+
this.separatePayloads = options.separatePayloads ?? false;
|
|
15042
15304
|
this.scheduler = options.scheduler;
|
|
15043
15305
|
this.logger = options.logger ?? new _trigger_dev_core_logger.Logger("FairQueue", "info");
|
|
15044
15306
|
this.abortController = new AbortController();
|
|
@@ -15046,7 +15308,7 @@ var FairQueue = class {
|
|
|
15046
15308
|
this.validateOnEnqueue = options.validateOnEnqueue ?? false;
|
|
15047
15309
|
this.retryStrategy = options.retry?.strategy;
|
|
15048
15310
|
this.deadLetterQueueEnabled = options.retry?.deadLetterQueue ?? true;
|
|
15049
|
-
this.shardCount = options.shardCount ?? 1;
|
|
15311
|
+
this.shardCount = options.keys.shardCount ?? options.shardCount ?? 1;
|
|
15050
15312
|
this.consumerCount = options.consumerCount ?? 1;
|
|
15051
15313
|
this.consumerIntervalMs = options.consumerIntervalMs ?? 100;
|
|
15052
15314
|
this.visibilityTimeoutMs = options.visibilityTimeoutMs ?? 3e4;
|
|
@@ -15138,6 +15400,9 @@ var FairQueue = class {
|
|
|
15138
15400
|
getDLQLength: async (tenantId) => {
|
|
15139
15401
|
return await this.getDeadLetterQueueLength(tenantId);
|
|
15140
15402
|
},
|
|
15403
|
+
getOldestMessageAge: async (shardId) => {
|
|
15404
|
+
return await this.getOldestMessageAge(shardId);
|
|
15405
|
+
},
|
|
15141
15406
|
shardCount: this.shardCount,
|
|
15142
15407
|
observedTenants: options?.observedTenants
|
|
15143
15408
|
});
|
|
@@ -15182,7 +15447,13 @@ var FairQueue = class {
|
|
|
15182
15447
|
}) : void 0,
|
|
15183
15448
|
metadata: options.metadata
|
|
15184
15449
|
};
|
|
15185
|
-
|
|
15450
|
+
if (this.separatePayloads) {
|
|
15451
|
+
const { payload, ...envelope } = storedMessage;
|
|
15452
|
+
const limit = options.ignorePayloadBytesLimit ? 0 : await this.options.tenantPayloadBytesLimit?.(options.tenantId) ?? 0;
|
|
15453
|
+
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));
|
|
15454
|
+
if (result === -1) throw new TenantQueueFullError(options.tenantId, limit);
|
|
15455
|
+
span.setAttribute("enqueued", result === 1);
|
|
15456
|
+
} else await this.redis.enqueueMessageAtomicV2(queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, options.queueId, messageId, timestamp.toString(), JSON.stringify(storedMessage), options.tenantId);
|
|
15186
15457
|
span.setAttributes({
|
|
15187
15458
|
[FairQueueAttributes.QUEUE_ID]: options.queueId,
|
|
15188
15459
|
[FairQueueAttributes.TENANT_ID]: options.tenantId,
|
|
@@ -15206,6 +15477,7 @@ var FairQueue = class {
|
|
|
15206
15477
|
*/
|
|
15207
15478
|
async enqueueBatch(options) {
|
|
15208
15479
|
return this.telemetry.trace("enqueueBatch", async (span) => {
|
|
15480
|
+
if (this.separatePayloads) throw new Error("enqueueBatch isn't supported with separatePayloads");
|
|
15209
15481
|
const queueKey = this.keys.queueKey(options.queueId);
|
|
15210
15482
|
const queueItemsKey = this.keys.queueItemsKey(options.queueId);
|
|
15211
15483
|
const dispatchShardId = this.tenantDispatch.getShardForTenant(options.tenantId);
|
|
@@ -15460,6 +15732,17 @@ var FairQueue = class {
|
|
|
15460
15732
|
return dispatchCount + legacyCount;
|
|
15461
15733
|
}
|
|
15462
15734
|
/**
|
|
15735
|
+
* Get the age in ms of the oldest due message not yet completed in a shard, or 0 if none.
|
|
15736
|
+
* Claims don't refresh the index scores, so claimed messages count until their queue next completes or empties.
|
|
15737
|
+
*/
|
|
15738
|
+
async getOldestMessageAge(shardId) {
|
|
15739
|
+
const now = Date.now();
|
|
15740
|
+
const [[oldestTenant], [oldestLegacyQueue]] = await Promise.all([this.tenantDispatch.getTenantsFromShard(shardId, 1, now), this.masterQueue.getQueuesFromShard(shardId, 1, now)]);
|
|
15741
|
+
const oldestScore = Math.min(oldestTenant?.score ?? Infinity, oldestLegacyQueue?.score ?? Infinity);
|
|
15742
|
+
if (!Number.isFinite(oldestScore)) return 0;
|
|
15743
|
+
return Math.max(0, now - oldestScore);
|
|
15744
|
+
}
|
|
15745
|
+
/**
|
|
15463
15746
|
* Get total in-flight message count.
|
|
15464
15747
|
*/
|
|
15465
15748
|
async getTotalInflightCount() {
|
|
@@ -15722,10 +16005,22 @@ var FairQueue = class {
|
|
|
15722
16005
|
async getMessageData(messageId, queueId) {
|
|
15723
16006
|
const shardId = this.masterQueue.getShardForQueue(queueId);
|
|
15724
16007
|
const inflightDataKey = this.keys.inflightDataKey(shardId);
|
|
15725
|
-
const dataJson = await this.redis.hget(inflightDataKey, messageId);
|
|
16008
|
+
const [dataJson, payloadJson] = await Promise.all([this.redis.hget(inflightDataKey, messageId), this.separatePayloads ? this.redis.get(this.keys.payloadKey(queueId, messageId)) : Promise.resolve(null)]);
|
|
15726
16009
|
if (!dataJson) return null;
|
|
15727
16010
|
try {
|
|
15728
|
-
|
|
16011
|
+
const message = JSON.parse(dataJson);
|
|
16012
|
+
if (!this.separatePayloads) return message;
|
|
16013
|
+
if (payloadJson === null) {
|
|
16014
|
+
this.logger.error("In-flight message has no payload", {
|
|
16015
|
+
messageId,
|
|
16016
|
+
queueId
|
|
16017
|
+
});
|
|
16018
|
+
return null;
|
|
16019
|
+
}
|
|
16020
|
+
return {
|
|
16021
|
+
...message,
|
|
16022
|
+
payload: JSON.parse(payloadJson)
|
|
16023
|
+
};
|
|
15729
16024
|
} catch {
|
|
15730
16025
|
this.logger.error("Failed to parse message data", {
|
|
15731
16026
|
messageId,
|
|
@@ -15766,7 +16061,7 @@ var FairQueue = class {
|
|
|
15766
16061
|
metadata: storedMessage?.metadata ?? {}
|
|
15767
16062
|
};
|
|
15768
16063
|
await this.#releaseConcurrencySafely(descriptor, messageId, "completeMessage");
|
|
15769
|
-
await this
|
|
16064
|
+
await this.#completeInVisibility(messageId, queueId, descriptor.tenantId);
|
|
15770
16065
|
const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId);
|
|
15771
16066
|
if (queueEmpty) {
|
|
15772
16067
|
this.queueDescriptorCache.delete(queueId);
|
|
@@ -15916,9 +16211,16 @@ var FairQueue = class {
|
|
|
15916
16211
|
}
|
|
15917
16212
|
async #moveToDeadLetterQueue(storedMessage, errorMessage) {
|
|
15918
16213
|
if (!this.deadLetterQueueEnabled) {
|
|
15919
|
-
await this
|
|
16214
|
+
await this.#completeInVisibility(storedMessage.id, storedMessage.queueId, storedMessage.tenantId);
|
|
15920
16215
|
return;
|
|
15921
16216
|
}
|
|
16217
|
+
if (this.separatePayloads) {
|
|
16218
|
+
const payloadJson = await this.redis.get(this.keys.payloadKey(storedMessage.queueId, storedMessage.id));
|
|
16219
|
+
storedMessage = {
|
|
16220
|
+
...storedMessage,
|
|
16221
|
+
payload: payloadJson === null ? void 0 : JSON.parse(payloadJson)
|
|
16222
|
+
};
|
|
16223
|
+
}
|
|
15922
16224
|
const dlqKey = this.keys.deadLetterQueueKey(storedMessage.tenantId);
|
|
15923
16225
|
const dlqDataKey = this.keys.deadLetterQueueDataKey(storedMessage.tenantId);
|
|
15924
16226
|
this.masterQueue.getShardForQueue(storedMessage.queueId);
|
|
@@ -15932,7 +16234,7 @@ var FairQueue = class {
|
|
|
15932
16234
|
lastError: errorMessage,
|
|
15933
16235
|
originalTimestamp: storedMessage.timestamp
|
|
15934
16236
|
};
|
|
15935
|
-
await this
|
|
16237
|
+
await this.#completeInVisibility(storedMessage.id, storedMessage.queueId, storedMessage.tenantId);
|
|
15936
16238
|
const pipeline = this.redis.pipeline();
|
|
15937
16239
|
pipeline.zadd(dlqKey, dlqMessage.deadLetteredAt, storedMessage.id);
|
|
15938
16240
|
pipeline.hset(dlqDataKey, storedMessage.id, JSON.stringify(dlqMessage));
|
|
@@ -16078,6 +16380,40 @@ var FairQueue = class {
|
|
|
16078
16380
|
const removedFromDispatch = await this.redis.updateDispatchIndexes(queueKey, tenantQueueIndexKey, dispatchKey, queueId, tenantId);
|
|
16079
16381
|
return { queueEmpty: removedFromMaster === 1 || removedFromDispatch === 1 };
|
|
16080
16382
|
}
|
|
16383
|
+
/**
|
|
16384
|
+
* Remove a settled message from in-flight. With separate payloads this also deletes the payload
|
|
16385
|
+
* and takes its bytes off the tenant's count, in one script, so the count never drifts.
|
|
16386
|
+
*/
|
|
16387
|
+
async #completeInVisibility(messageId, queueId, tenantId) {
|
|
16388
|
+
if (!this.separatePayloads) {
|
|
16389
|
+
await this.visibilityManager.complete(messageId, queueId);
|
|
16390
|
+
return;
|
|
16391
|
+
}
|
|
16392
|
+
const shardId = this.masterQueue.getShardForQueue(queueId);
|
|
16393
|
+
await this.redis.completeSeparatePayload(this.keys.inflightKey(shardId), this.keys.inflightDataKey(shardId), this.keys.payloadKey(queueId, messageId), this.keys.tenantPayloadBytesKey(tenantId), `${messageId}:${queueId}`, messageId);
|
|
16394
|
+
}
|
|
16395
|
+
/** A tenant's queued and in-flight payload bytes, with `separatePayloads`. */
|
|
16396
|
+
async getTenantPayloadBytes(tenantId) {
|
|
16397
|
+
if (!this.separatePayloads) return 0;
|
|
16398
|
+
return Number(await this.redis.get(this.keys.tenantPayloadBytesKey(tenantId)) ?? 0);
|
|
16399
|
+
}
|
|
16400
|
+
/**
|
|
16401
|
+
* A cluster needs every script's keys in one slot, which only a tenant-sharded key producer
|
|
16402
|
+
* gives, and the concurrency reservation touches every group's set at once, so it allows one
|
|
16403
|
+
* group, keyed by tenant.
|
|
16404
|
+
*/
|
|
16405
|
+
static #validateLayout(options) {
|
|
16406
|
+
const keys = options.keys;
|
|
16407
|
+
if (options.separatePayloads && (!keys.payloadKey || !keys.tenantPayloadBytesKey)) throw new Error("separatePayloads needs a key producer with payloadKey and tenantPayloadBytesKey");
|
|
16408
|
+
if (keys.tenantShard) {
|
|
16409
|
+
if (!keys.shardCount) throw new Error("A key producer with tenantShard must set shardCount");
|
|
16410
|
+
if (options.shardCount !== void 0 && options.shardCount !== keys.shardCount) throw new Error(`shardCount ${options.shardCount} doesn't match the key producer's ${keys.shardCount}`);
|
|
16411
|
+
}
|
|
16412
|
+
if (isClusterConnection(options.redis)) {
|
|
16413
|
+
if (!keys.tenantShard) throw new Error("A cluster connection needs a tenant-sharded key producer");
|
|
16414
|
+
if ((options.concurrencyGroups?.length ?? 0) > 1) throw new Error("A cluster connection supports at most one concurrency group");
|
|
16415
|
+
}
|
|
16416
|
+
}
|
|
16081
16417
|
#createSchedulerContext() {
|
|
16082
16418
|
return {
|
|
16083
16419
|
getCurrentConcurrency: async (groupName, groupId) => {
|
|
@@ -16236,6 +16572,77 @@ if #tenantOldest >= 2 then
|
|
|
16236
16572
|
end
|
|
16237
16573
|
|
|
16238
16574
|
return (#ARGV - 2) / 3
|
|
16575
|
+
`
|
|
16576
|
+
});
|
|
16577
|
+
this.redis.defineCommand("enqueueMessageSeparatePayload", {
|
|
16578
|
+
numberOfKeys: 7,
|
|
16579
|
+
lua: `#!lua
|
|
16580
|
+
local queueKey = KEYS[1]
|
|
16581
|
+
local queueItemsKey = KEYS[2]
|
|
16582
|
+
local tenantQueueIndexKey = KEYS[3]
|
|
16583
|
+
local dispatchKey = KEYS[4]
|
|
16584
|
+
local payloadKey = KEYS[5]
|
|
16585
|
+
local inflightKey = KEYS[6]
|
|
16586
|
+
local bytesKey = KEYS[7]
|
|
16587
|
+
|
|
16588
|
+
local queueId = ARGV[1]
|
|
16589
|
+
local messageId = ARGV[2]
|
|
16590
|
+
local timestamp = tonumber(ARGV[3])
|
|
16591
|
+
local envelope = ARGV[4]
|
|
16592
|
+
local tenantId = ARGV[5]
|
|
16593
|
+
local payload = ARGV[6]
|
|
16594
|
+
local inflightMember = ARGV[7]
|
|
16595
|
+
local limit = tonumber(ARGV[8])
|
|
16596
|
+
|
|
16597
|
+
if redis.call('HEXISTS', queueItemsKey, messageId) == 1 then
|
|
16598
|
+
return 0
|
|
16599
|
+
end
|
|
16600
|
+
if redis.call('ZSCORE', inflightKey, inflightMember) then
|
|
16601
|
+
return 0
|
|
16602
|
+
end
|
|
16603
|
+
|
|
16604
|
+
local size = string.len(payload)
|
|
16605
|
+
if limit > 0 and tonumber(redis.call('GET', bytesKey) or '0') + size > limit then
|
|
16606
|
+
return -1
|
|
16607
|
+
end
|
|
16608
|
+
|
|
16609
|
+
redis.call('SET', payloadKey, payload)
|
|
16610
|
+
redis.call('INCRBY', bytesKey, size)
|
|
16611
|
+
redis.call('ZADD', queueKey, timestamp, messageId)
|
|
16612
|
+
redis.call('HSET', queueItemsKey, messageId, envelope)
|
|
16613
|
+
|
|
16614
|
+
local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
|
|
16615
|
+
if #oldest >= 2 then
|
|
16616
|
+
redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
|
|
16617
|
+
end
|
|
16618
|
+
|
|
16619
|
+
local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
|
|
16620
|
+
if #tenantOldest >= 2 then
|
|
16621
|
+
redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
|
|
16622
|
+
end
|
|
16623
|
+
|
|
16624
|
+
return 1
|
|
16625
|
+
`
|
|
16626
|
+
});
|
|
16627
|
+
this.redis.defineCommand("completeSeparatePayload", {
|
|
16628
|
+
numberOfKeys: 4,
|
|
16629
|
+
lua: `
|
|
16630
|
+
local inflightKey = KEYS[1]
|
|
16631
|
+
local inflightDataKey = KEYS[2]
|
|
16632
|
+
local payloadKey = KEYS[3]
|
|
16633
|
+
local bytesKey = KEYS[4]
|
|
16634
|
+
|
|
16635
|
+
redis.call('ZREM', inflightKey, ARGV[1])
|
|
16636
|
+
redis.call('HDEL', inflightDataKey, ARGV[2])
|
|
16637
|
+
|
|
16638
|
+
local size = redis.call('STRLEN', payloadKey)
|
|
16639
|
+
if size > 0 then
|
|
16640
|
+
redis.call('DEL', payloadKey)
|
|
16641
|
+
if redis.call('DECRBY', bytesKey, size) <= 0 then
|
|
16642
|
+
redis.call('DEL', bytesKey)
|
|
16643
|
+
end
|
|
16644
|
+
end
|
|
16645
|
+
return size
|
|
16239
16646
|
`
|
|
16240
16647
|
});
|
|
16241
16648
|
this.redis.defineCommand("updateDispatchIndexes", {
|
|
@@ -17385,16 +17792,22 @@ exports.NoopScheduler = NoopScheduler;
|
|
|
17385
17792
|
exports.RoundRobinScheduler = RoundRobinScheduler;
|
|
17386
17793
|
exports.SimpleQueue = SimpleQueue;
|
|
17387
17794
|
exports.TenantDispatch = TenantDispatch;
|
|
17795
|
+
exports.TenantQueueFullError = TenantQueueFullError;
|
|
17796
|
+
exports.TenantShardedFairQueueKeyProducer = TenantShardedFairQueueKeyProducer;
|
|
17388
17797
|
exports.VisibilityManager = VisibilityManager;
|
|
17389
17798
|
exports.WeightedScheduler = WeightedScheduler;
|
|
17390
17799
|
exports.Worker = Worker;
|
|
17391
17800
|
exports.WorkerQueueManager = WorkerQueueManager;
|
|
17392
17801
|
exports.createDefaultRetryStrategy = createDefaultRetryStrategy;
|
|
17802
|
+
exports.createFairQueueRedisClient = createFairQueueRedisClient;
|
|
17393
17803
|
exports.defaultRetryOptions = defaultRetryOptions;
|
|
17394
17804
|
exports.deserialiseSnapshot = deserialiseSnapshot;
|
|
17395
17805
|
exports.idempotencyLookupKeyFor = idempotencyLookupKeyFor;
|
|
17396
17806
|
exports.isAbortError = isAbortError;
|
|
17807
|
+
exports.isClusterConnection = isClusterConnection;
|
|
17808
|
+
exports.keyPrefixOf = keyPrefixOf;
|
|
17397
17809
|
exports.makeIdempotencyClaimKey = makeIdempotencyClaimKey;
|
|
17398
17810
|
exports.noopTelemetry = noopTelemetry;
|
|
17811
|
+
exports.scanKeys = scanKeys;
|
|
17399
17812
|
exports.serialiseSnapshot = serialiseSnapshot;
|
|
17400
17813
|
//# sourceMappingURL=index.cjs.map
|