bullmq 5.80.7 → 5.80.9

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlowProducer = void 0;
4
4
  const events_1 = require("events");
5
5
  const utils_1 = require("../utils");
6
+ const create_scripts_1 = require("../utils/create-scripts");
6
7
  const job_1 = require("./job");
7
8
  const queue_keys_1 = require("./queue-keys");
8
9
  const redis_connection_1 = require("./redis-connection");
@@ -19,6 +20,13 @@ class FlowProducer extends events_1.EventEmitter {
19
20
  constructor(opts = { connection: {} }, Connection = redis_connection_1.RedisConnection) {
20
21
  super();
21
22
  this.opts = opts;
23
+ /**
24
+ * Cache of lightweight queue-like objects keyed by `${prefix}:${queueName}`.
25
+ * Each entry carries a single shared Scripts instance that is reused by all
26
+ * jobs added to that queue during the FlowProducer's lifetime, avoiding a
27
+ * redundant Scripts allocation per node/job.
28
+ */
29
+ this.queues = new Map();
22
30
  this.opts = Object.assign({ prefix: 'bull' }, opts);
23
31
  this.connection = new Connection(opts.connection, {
24
32
  shared: (0, utils_1.isRedisInstance)(opts.connection),
@@ -28,6 +36,7 @@ class FlowProducer extends events_1.EventEmitter {
28
36
  });
29
37
  this.connection.on('error', (error) => this.emit('error', error));
30
38
  this.connection.on('close', () => {
39
+ this.queues.clear();
31
40
  if (!this.closing) {
32
41
  this.emit('ioredis:close');
33
42
  }
@@ -351,22 +360,40 @@ class FlowProducer extends events_1.EventEmitter {
351
360
  * @returns A queue-like object with the client, keys, and options needed to create jobs.
352
361
  */
353
362
  queueFromNode(node, queueKeys, prefix) {
354
- return {
363
+ const cacheKey = `${prefix}:${node.queueName}`;
364
+ const cached = this.queues.get(cacheKey);
365
+ if (cached) {
366
+ return cached;
367
+ }
368
+ const flowProducer = this;
369
+ const queue = {
355
370
  client: this.connection.client,
356
371
  name: node.queueName,
357
372
  keys: queueKeys.getKeys(node.queueName),
358
373
  toKey: (type) => queueKeys.toKey(node.queueName, type),
359
374
  opts: { prefix, connection: {} },
360
375
  qualifiedName: queueKeys.getQueueQualifiedName(node.queueName),
361
- closing: this.closing,
376
+ get closing() {
377
+ return flowProducer.closing;
378
+ },
362
379
  waitUntilReady: async () => this.connection.client,
363
380
  removeListener: this.removeListener.bind(this),
364
381
  emit: this.emit.bind(this),
365
382
  on: this.on.bind(this),
366
- redisVersion: this.connection.redisVersion,
367
- databaseType: this.connection.databaseType,
383
+ get redisVersion() {
384
+ return flowProducer.connection.redisVersion;
385
+ },
386
+ get databaseType() {
387
+ return flowProducer.connection.databaseType;
388
+ },
368
389
  trace: async () => { },
369
390
  };
391
+ // Build the shared Scripts instance once per queue so that every job
392
+ // created from this queue-like object reuses it instead of allocating
393
+ // its own.
394
+ queue.scripts = (0, create_scripts_1.createScripts)(queue);
395
+ this.queues.set(cacheKey, queue);
396
+ return queue;
370
397
  }
371
398
  /**
372
399
  * Translates numeric addJob Lua error codes returned by root flow exec.
@@ -395,6 +422,7 @@ class FlowProducer extends events_1.EventEmitter {
395
422
  */
396
423
  async close() {
397
424
  if (!this.closing) {
425
+ this.queues.clear();
398
426
  this.closing = this.connection.close();
399
427
  }
400
428
  await this.closing;
@@ -243,7 +243,13 @@ class Job {
243
243
  return job;
244
244
  }
245
245
  createScripts() {
246
- this.scripts = (0, create_scripts_1.createScripts)(this.queue);
246
+ var _a;
247
+ // Reuse the queue's long-lived Scripts instance. It is bound to the same
248
+ // queue keys and connection, so a per-Job instance is pure duplication.
249
+ // The built-in Queue, Worker and FlowProducer always provide one; the
250
+ // fallback only covers custom queue-like objects that don't carry a
251
+ // Scripts instance.
252
+ this.scripts = (_a = this.queue.scripts) !== null && _a !== void 0 ? _a : (0, create_scripts_1.createScripts)(this.queue);
247
253
  }
248
254
  static optsFromJSON(rawOpts, optsDecode = utils_1.optsDecodeMap) {
249
255
  const opts = JSON.parse(rawOpts || '{}');
@@ -297,7 +303,8 @@ class Job {
297
303
  * @returns The total number of log entries for this job so far.
298
304
  */
299
305
  static addJobLog(queue, jobId, logRow, keepLogs) {
300
- const scripts = queue.scripts;
306
+ var _a;
307
+ const scripts = (_a = queue.scripts) !== null && _a !== void 0 ? _a : (0, create_scripts_1.createScripts)(queue);
301
308
  return scripts.addLog(jobId, logRow, keepLogs);
302
309
  }
303
310
  toJSON() {
@@ -144,23 +144,31 @@ end
144
144
  local jobId = "repeat:" .. jobSchedulerId .. ":" .. nextMillis
145
145
  local jobKey = prefixKey .. jobId
146
146
 
147
- -- If there's already a job with this ID, in a state
148
- -- that is not updatable (active, completed, failed) we must
147
+ -- If there's already a job with this ID, in a state
148
+ -- that is not updatable (active, completed, failed) we must
149
149
  -- handle the collision
150
150
  local hasCollision = false
151
151
  if rcall("EXISTS", jobKey) == 1 then
152
152
  if every then
153
- -- For 'every' case: try next time slot to avoid collision
154
- local nextSlotMillis = nextMillis + every
155
- local nextSlotJobId = "repeat:" .. jobSchedulerId .. ":" .. nextSlotMillis
156
- local nextSlotJobKey = prefixKey .. nextSlotJobId
157
-
158
- if rcall("EXISTS", nextSlotJobKey) == 0 then
159
- -- Next slot is free, use it
160
- nextMillis = nextSlotMillis
161
- jobId = nextSlotJobId
162
- else
163
- -- Next slot also has a job, return error code
153
+ -- For 'every' case: walk forward through subsequent slots
154
+ -- until we find a free one. Stale completed/failed jobs from
155
+ -- a previous scheduler under the same id can occupy several
156
+ -- consecutive slots (issue #3063), so a single retry is not
157
+ -- enough. The scan is bounded so we don't spin if the
158
+ -- scheduler is genuinely contested.
159
+ local maxSlotScans = 32
160
+ local slotsScanned = 0
161
+ local jobExists
162
+ repeat
163
+ nextMillis = nextMillis + every
164
+ jobId = "repeat:" .. jobSchedulerId .. ":" .. nextMillis
165
+ jobKey = prefixKey .. jobId
166
+ slotsScanned = slotsScanned + 1
167
+ jobExists = rcall("EXISTS", jobKey)
168
+ until jobExists == 0 or slotsScanned >= maxSlotScans
169
+
170
+ if jobExists == 1 then
171
+ -- Every scanned slot still has a job, return error code
164
172
  return -11 -- SchedulerJobSlotsBusy
165
173
  end
166
174
  else
@@ -553,22 +553,30 @@ end
553
553
  -- Check for job ID collision with existing jobs (in any state)
554
554
  local jobId = "repeat:" .. jobSchedulerId .. ":" .. nextMillis
555
555
  local jobKey = prefixKey .. jobId
556
- -- If there's already a job with this ID, in a state
557
- -- that is not updatable (active, completed, failed) we must
556
+ -- If there's already a job with this ID, in a state
557
+ -- that is not updatable (active, completed, failed) we must
558
558
  -- handle the collision
559
559
  local hasCollision = false
560
560
  if rcall("EXISTS", jobKey) == 1 then
561
561
  if every then
562
- -- For 'every' case: try next time slot to avoid collision
563
- local nextSlotMillis = nextMillis + every
564
- local nextSlotJobId = "repeat:" .. jobSchedulerId .. ":" .. nextSlotMillis
565
- local nextSlotJobKey = prefixKey .. nextSlotJobId
566
- if rcall("EXISTS", nextSlotJobKey) == 0 then
567
- -- Next slot is free, use it
568
- nextMillis = nextSlotMillis
569
- jobId = nextSlotJobId
570
- else
571
- -- Next slot also has a job, return error code
562
+ -- For 'every' case: walk forward through subsequent slots
563
+ -- until we find a free one. Stale completed/failed jobs from
564
+ -- a previous scheduler under the same id can occupy several
565
+ -- consecutive slots (issue #3063), so a single retry is not
566
+ -- enough. The scan is bounded so we don't spin if the
567
+ -- scheduler is genuinely contested.
568
+ local maxSlotScans = 32
569
+ local slotsScanned = 0
570
+ local jobExists
571
+ repeat
572
+ nextMillis = nextMillis + every
573
+ jobId = "repeat:" .. jobSchedulerId .. ":" .. nextMillis
574
+ jobKey = prefixKey .. jobId
575
+ slotsScanned = slotsScanned + 1
576
+ jobExists = rcall("EXISTS", jobKey)
577
+ until jobExists == 0 or slotsScanned >= maxSlotScans
578
+ if jobExists == 1 then
579
+ -- Every scanned slot still has a job, return error code
572
580
  return -11 -- SchedulerJobSlotsBusy
573
581
  end
574
582
  else