bullmq 5.80.4 → 5.80.6

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.
@@ -46,7 +46,23 @@ class ChildPool {
46
46
  }
47
47
  catch (err) {
48
48
  console.error(err);
49
- this.release(child);
49
+ // A child that failed to initialize (or exited during init) must never
50
+ // be released back into the free pool, otherwise it becomes a "zombie"
51
+ // that is reused for every subsequent job and fails them instantly.
52
+ // Kill and remove it so a fresh child is forked on the next retain.
53
+ // The child also exits itself after a failed init (see ChildProcessor),
54
+ // so this is normally a no-op; log any kill failure instead of silently
55
+ // swallowing it so a lingering child would not go unnoticed.
56
+ if (child.childProcess || child.worker) {
57
+ try {
58
+ this.kill(child, 'SIGKILL').catch(killErr => {
59
+ console.error('Failed to kill child after init error:', killErr);
60
+ });
61
+ }
62
+ catch (killErr) {
63
+ console.error('Failed to kill child after init error:', killErr);
64
+ }
65
+ }
50
66
  throw err;
51
67
  }
52
68
  }
@@ -25,6 +25,7 @@ class ChildProcessor {
25
25
  this.receiver = receiver;
26
26
  }
27
27
  async init(processorFile) {
28
+ var _a;
28
29
  let processor;
29
30
  try {
30
31
  const { default: processorFn } = await import(processorFile);
@@ -39,10 +40,22 @@ class ChildProcessor {
39
40
  }
40
41
  catch (err) {
41
42
  this.status = ChildStatus.Errored;
42
- return this.send({
43
- cmd: enums_1.ParentCommand.InitFailed,
44
- err: (0, utils_1.errorToJSON)(err),
45
- });
43
+ try {
44
+ await this.send({
45
+ cmd: enums_1.ParentCommand.InitFailed,
46
+ err: (0, utils_1.errorToJSON)(err),
47
+ });
48
+ }
49
+ finally {
50
+ // A child that failed to initialize cannot recover, and because the open
51
+ // IPC channel keeps its event loop alive it would never exit on its own.
52
+ // Exit explicitly (after attempting to send InitFailed) so the parent
53
+ // can never reuse a half-initialized "zombie" child. This is a
54
+ // belt-and-braces measure: ChildPool also kills the child, but exiting
55
+ // here guarantees termination even if the parent-side kill were to fail.
56
+ // In a worker thread this stops only the current worker, not the process.
57
+ process.exit((_a = process.exitCode) !== null && _a !== void 0 ? _a : 1);
58
+ }
46
59
  }
47
60
  const origProcessor = processor;
48
61
  processor = function (job, token, signal) {
@@ -68,6 +68,7 @@ class Child extends events_1.EventEmitter {
68
68
  return this._killed;
69
69
  }
70
70
  async init() {
71
+ var _a, _b;
71
72
  const execArgv = await convertExecArgv(process.execArgv);
72
73
  let parent;
73
74
  if (this.opts.useWorkerThreads) {
@@ -92,8 +93,13 @@ class Child extends events_1.EventEmitter {
92
93
  parent.on('error', (...args) => this.emit('error', ...args));
93
94
  parent.on('message', (...args) => this.emit('message', ...args));
94
95
  parent.on('close', (...args) => this.emit('close', ...args));
95
- parent.stdout.pipe(process.stdout);
96
- parent.stderr.pipe(process.stderr);
96
+ // `parent.stdout`/`parent.stderr` may be null when the underlying runtime
97
+ // does not pipe child stdio (e.g. Bun ignores `worker_threads` stdout/stderr
98
+ // options, and Node returns null when `stdio: 'ignore'` is passed in
99
+ // `workerForkOptions`). Guard the pipe calls so initialization does not throw.
100
+ // See https://github.com/taskforcesh/bullmq/issues/2232
101
+ (_a = parent.stdout) === null || _a === void 0 ? void 0 : _a.pipe(process.stdout);
102
+ (_b = parent.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
97
103
  await this.initChild();
98
104
  }
99
105
  async send(msg) {
@@ -27,6 +27,18 @@ class Job {
27
27
  name,
28
28
  /**
29
29
  * The payload for this job.
30
+ *
31
+ * Note: The `DataType` type parameter describes the *shape* of the
32
+ * payload, not its runtime class. Job data is serialized with
33
+ * `JSON.stringify` before being stored in Redis and deserialized with
34
+ * `JSON.parse` when read back, so when a worker picks up the job
35
+ * `job.data` will contain a JSON-compatible value such as an object,
36
+ * array, string, number, boolean, or `null`. Class instances passed as
37
+ * `data` lose their prototype (and therefore any methods,
38
+ * getters/setters, or non-enumerable properties) on the worker side.
39
+ * If you need methods, store the data as a plain object and
40
+ * re-instantiate the class inside your processor, or rely on
41
+ * `JSON.stringify`'s built-in `toJSON()` hook to control serialization.
30
42
  */
31
43
  data,
32
44
  /**
@@ -120,7 +132,11 @@ class Job {
120
132
  *
121
133
  * @param queue - the queue where to add the job.
122
134
  * @param name - the name of the job.
123
- * @param data - the payload of the job.
135
+ * @param data - the payload of the job. It will be serialized with
136
+ * `JSON.stringify` before being stored in Redis, so it must be
137
+ * JSON-serializable. Class instances are flattened to plain objects and
138
+ * lose their prototype methods on the worker side; see the
139
+ * {@link Job} class for details.
124
140
  * @param opts - the options bag for this job.
125
141
  * @returns The created Job instance
126
142
  */
@@ -145,7 +145,14 @@ class Queue extends queue_getters_1.QueueGetters {
145
145
  * Adds a new job to the queue.
146
146
  *
147
147
  * @param name - Name of the job to be added to the queue.
148
- * @param data - Arbitrary data to append to the job.
148
+ * @param data - Arbitrary data to append to the job. The value is
149
+ * serialized with `JSON.stringify` before being stored in Redis, so it
150
+ * must be JSON-serializable. The `DataType` type parameter describes the
151
+ * shape of this payload, not a runtime class: passing a class instance
152
+ * will store its enumerable own properties, but its prototype methods
153
+ * and getters/setters will not be available when a worker reads
154
+ * `job.data` back. Prefer plain objects, or implement `toJSON()` on
155
+ * the class to control how it is serialized.
149
156
  * @param opts - Job options that affects how the job is going to be processed.
150
157
  */
151
158
  async add(name, data, opts) {
@@ -200,7 +207,9 @@ class Queue extends queue_getters_1.QueueGetters {
200
207
  * one job at a time in a sequence.
201
208
  *
202
209
  * @param jobs - The array of jobs to add to the queue. Each job is defined by 3
203
- * properties, 'name', 'data' and 'opts'. They follow the same signature as 'Queue.add'.
210
+ * properties, 'name', 'data' and 'opts'. They follow the same signature as 'Queue.add',
211
+ * including the JSON-serialization caveat for `data` (class instances lose their
212
+ * prototype methods on the worker side).
204
213
  */
205
214
  async addBulk(jobs) {
206
215
  return this.trace(enums_1.SpanKind.PRODUCER, 'addBulk', this.name, async (span, srcPropagationMetadata) => {
@@ -17,7 +17,7 @@ const sandbox = (processFile, childPool) => {
17
17
  child = await childPool.retain(processFile);
18
18
  child.on('exit', exitHandler);
19
19
  msgHandler = async (msg) => {
20
- var _a, _b, _c, _d, _e;
20
+ var _a, _b, _c, _d, _e, _f;
21
21
  try {
22
22
  switch (msg.cmd) {
23
23
  case enums_1.ParentCommand.Completed:
@@ -26,7 +26,10 @@ const sandbox = (processFile, childPool) => {
26
26
  case enums_1.ParentCommand.Failed:
27
27
  case enums_1.ParentCommand.Error: {
28
28
  const err = new Error();
29
- Object.assign(err, msg.value);
29
+ // ParentCommand.Failed carries the error under `value`,
30
+ // while ParentCommand.Error carries it under `err`. Read
31
+ // from either key so the failure reason is never lost.
32
+ Object.assign(err, (_a = msg.value) !== null && _a !== void 0 ? _a : msg.err);
30
33
  reject(err);
31
34
  break;
32
35
  }
@@ -37,14 +40,14 @@ const sandbox = (processFile, childPool) => {
37
40
  await job.log(msg.value);
38
41
  break;
39
42
  case enums_1.ParentCommand.MoveToDelayed:
40
- await job.moveToDelayed((_a = msg.value) === null || _a === void 0 ? void 0 : _a.timestamp, (_b = msg.value) === null || _b === void 0 ? void 0 : _b.token);
43
+ await job.moveToDelayed((_b = msg.value) === null || _b === void 0 ? void 0 : _b.timestamp, (_c = msg.value) === null || _c === void 0 ? void 0 : _c.token);
41
44
  break;
42
45
  case enums_1.ParentCommand.MoveToWait:
43
- await job.moveToWait((_c = msg.value) === null || _c === void 0 ? void 0 : _c.token);
46
+ await job.moveToWait((_d = msg.value) === null || _d === void 0 ? void 0 : _d.token);
44
47
  break;
45
48
  case enums_1.ParentCommand.MoveToWaitingChildren:
46
49
  {
47
- const value = await job.moveToWaitingChildren((_d = msg.value) === null || _d === void 0 ? void 0 : _d.token, (_e = msg.value) === null || _e === void 0 ? void 0 : _e.opts);
50
+ const value = await job.moveToWaitingChildren((_e = msg.value) === null || _e === void 0 ? void 0 : _e.token, (_f = msg.value) === null || _f === void 0 ? void 0 : _f.opts);
48
51
  child.send({
49
52
  requestId: msg.requestId,
50
53
  cmd: enums_1.ChildCommand.MoveToWaitingChildrenResponse,