Skip to content

feat: add reuseChildProcess option to disable child process reuse - #4091

Open
samwisekind wants to merge 7 commits into
taskforcesh:masterfrom
ceros:master
Open

feat: add reuseChildProcess option to disable child process reuse#4091
samwisekind wants to merge 7 commits into
taskforcesh:masterfrom
ceros:master

Conversation

@samwisekind

Copy link
Copy Markdown

Why

Resolves #2422

By default BullMQ reuses sandboxed child processes across jobs for performance. However, if a processor or its dependencies have known memory leaks, memory usage can grow unboundedly over time. There is currently no way to opt out of this behaviour.

The optional reuseChildProcess property (when set to false) terminates the child process after each job completes, so every job runs in a fresh process with clean memory.

How

  • Added reuseChildProcess?: boolean (default true) to SandboxedOptions
  • When reuseChildProcess is false, it kills the child via SIGTERM instead of returning it to the free pool
  • Changed ChildPool.release() from sync to async because the kill() method it now conditionally calls is already async
    • We can keep it sync and essentially fire-and-forget the kill, but it would be difficult to guarantee the child is fully terminated before moving on
  • Updated sandbox.ts to await the now-async release() call
  • Added unit tests in child-pool.test.ts verifying that children are killed on release and that new children are spawned after release
  • Added integration tests in sandboxed_process.test.ts that assert PIDs differ across jobs when reuse is disabled, and match when reuse is enabled
  • Added documentation in the sandboxed processors guide with a performance warning

Additional Notes (Optional)

  • Changes are fully optional and backward compatible (defaults to true to match current behaviour)
  • Only concern would be the async change to ChildPool.release()

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-out for sandboxed child process/worker thread reuse so users can avoid long-lived memory growth from leaky processors/dependencies by terminating the sandbox after each job.

Changes:

  • Introduces reuseChildProcess?: boolean (default true) in sandboxed/worker options and wires it into Worker -> ChildPool.
  • Updates ChildPool.release() to optionally terminate the child instead of returning it to the free pool (and makes release() async); updates sandbox cleanup accordingly.
  • Adds unit/integration tests and updates sandboxed processors documentation to describe the new option and its performance tradeoff.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/sandboxed_process.test.ts Integration tests asserting PID reuse vs non-reuse across jobs.
tests/child-pool.test.ts Unit tests for ChildPool release/kill behavior and updated async release usage.
src/interfaces/sandboxed-options.ts Adds the reuseChildProcess option to the public options surface.
src/classes/worker.ts Passes reuseChildProcess into the ChildPool created for sandboxed processors.
src/classes/sandbox.ts Awaits the now-async childPool.release() during sandbox cleanup.
src/classes/child-pool.ts Implements conditional termination on release and makes release() async.
docs/gitbook/guide/workers/sandboxed-processors.md Documents disabling reuse and warns about throughput impact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/classes/child-pool.ts
Comment on lines +79 to +85
async release(child: Child): Promise<void> {
delete this.retained[child.pid];
this.getFree(child.processFile).push(child);
if (this.reuseChildProcess) {
this.getFree(child.processFile).push(child);
} else {
await this.kill(child, 'SIGTERM');
}

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release() now awaits kill(). Child.kill() currently has a race where the underlying process/thread can exit between hasProcessExited() and registering the once('exit') listener, which can leave the awaited onExit promise unresolved indefinitely. With reuseChildProcess: false this path runs after every job, so this can hang job completion. Consider making Child.kill() race-safe (attach listener first, then re-check exited state and resolve/remove listener if already exited), and/or add a bounded fallback in ChildPool.kill() to avoid awaiting forever.

Copilot uses AI. Check for mistakes.

@manast manast Apr 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, this was not even part of the PR, but nevertheless seems important for this new option to avoid workers that could get stuck forever.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manast Happy to update the PR tackling this (and the typo comments below) or as a quick follow-up!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with de626a0

Comment thread src/classes/child-pool.ts
Comment thread src/classes/child-pool.ts
Comment on lines +81 to +85
if (this.reuseChildProcess) {
this.getFree(child.processFile).push(child);
} else {
await this.kill(child, 'SIGTERM');
}

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reuseChildProcess is false, release() will await a full termination with CHILD_KILL_TIMEOUT = 30_000. That extends the processor's perceived runtime and delays the job being moved to completed/failed, potentially reducing throughput or causing stalls if shutdown is slow. Consider using a shorter timeout for the per-job termination path and/or making the timeout configurable for this option.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is an issue in practice.

Comment thread docs/gitbook/guide/workers/sandboxed-processors.md Outdated
Comment thread docs/gitbook/guide/workers/sandboxed-processors.md Outdated
Comment thread docs/gitbook/guide/workers/sandboxed-processors.md Outdated

@manast manast left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@samwisekind
samwisekind requested a review from manast April 27, 2026 11:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread src/classes/child.ts
Comment on lines +176 to +185
const escalate = new Promise<void>(resolve =>
setTimeout(() => {
if (!this.hasProcessExited()) {
this.killProcess('SIGKILL');
}
resolve();
}, timeoutMs),
);

await Promise.race([onExit, escalate]);
Comment thread src/classes/child.ts
Comment on lines +176 to +185
const escalate = new Promise<void>(resolve =>
setTimeout(() => {
if (!this.hasProcessExited()) {
this.killProcess('SIGKILL');
}
resolve();
}, timeoutMs),
);

await Promise.race([onExit, escalate]);
@manast

manast commented May 28, 2026

Copy link
Copy Markdown
Contributor

@copilot please address the PR comments.

@samwisekind

Copy link
Copy Markdown
Author

@manast I think there was a copilot outage last week 😅 let me know if you want me to apply the code suggestions manually!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Option not to reuse child process

3 participants