Release 19.0.0
This release closes several ways a child process could be leaked, and several ways a task's promise
could be dropped. It is major because some fixes change runtime behavior for every consumer, even
though no API was removed or renamed.
-
💔 BREAKING: outstanding work now keeps the event loop alive.
Everything this library owns is deliberately unref'd, so an idle cluster never stops a script from
exiting. But unsettled work held nothing open either: when the loop drained,beforeExitfired
andend()tore down a task the caller was still awaiting. A queued task's promise was
abandoned entirely — node exited 0 with no error at all — and an assigned task was rejected
with "Process terminated before task completed". The only thing that ever prevented this was the
per-task timeout timer, which doesn't exist at the defaulttaskTimeoutMillisof 0.This is longstanding, not new in v19. An idle cluster still holds nothing, so the
unrefStreams
contract is unchanged. One consequence: if a child wedges and you haven't settaskTimeoutMillis,
your process now stays alive rather than exiting silently. A visible hang beats losing work
without a diagnostic, but it is a good reason to set that option. -
💔 BREAKING:
end()is a barrier. It previously drained the pool and resolved, which is not
the same as "every child is gone": an in-flightprocessFactory()could hand back a live child
afterwards, and a recycling process could still be seconds into its graceful shutdown. Either way,
await bc.end(); process.exit(0)could orphan a child.Explicit
end()now waits for in-flight spawns and recycling, so it can take longer than it used
to — and deliberately has no deadline, because until a factory returns we cannot know whether it
already spawned a child. A factory that never settles keeps explicit shutdown pending.Automatic cleanup on
beforeExitstays bounded byspawnTimeoutMillis, since process exit can't
wait forever on an opaque factory. When that bound expires, every child known to be alive is
force-killed and any task still waiting on those terminations is rejected — unless you set
cleanupChildProcs: false, which means you handle PID cleanup yourself. -
💔 BREAKING:
end()rejects tasks still queued, with
BatchCluster.end() was called before this task could be assigned: <command>. Nothing settled
them before, so callingend()with work outstanding now yields rejections where you previously
got promises that never settled. -
💔 BREAKING:
maxFailedTasksPerProcessdefaults to0(disabled), and works when you set it.failedTaskCountwas never incremented, so the previous default of2never recycled anything.
Rather than silently enabling that rule for everyone, it is now off by default: a rejected task
usually means bad input rather than a sick child, and it counts failures over the process's whole
lifetime, so a long-lived child would be recycled after any two bad inputs, ever. If you set it
explicitly, expect more churn and"broken"inchildEndCounts. PreferhealthCheckCommandif
you can ask the child directly. -
💔 BREAKING:
kill()returnsfalseonEPERMinstead of throwing, matching how it already
handledESRCH; unrecognized codes still throw. Callers signal lists of pids in loops, and one
pid we aren't permitted to signal stranded every pid after it. -
💔 BREAKING (TypeScript only):
killProcessGroupis a required property of the exported
InternalBatchProcessOptionsandCombinedBatchProcessOptionstypes, so code building either by
hand needs the new field. Passing options to theBatchClusterconstructor is unaffected. -
✨ Added the
killProcessGroupoption (defaultfalse) and exportedkillGroup().Enable it if your
processFactoryusesdetached: true, which makes each child its own process
group leader: shutdown then keeps ownership of that group even after the leader exits, so
grandchildren are cleaned up too. Safe but pointless for non-detached children — the group signal
simply fails and we signal the child directly. Windows has no POSIX process groups and ignores it. -
🐞 Fixed two outright child-process leaks. If
new BatchProcess()threw after the factory
returned a live child — a factory whosestdioomits stdin or stdout, or achildStartlistener
that throws — the child never entered the pool, leaving it invisible topids(), recycling,
end(), and the exit backstop, one orphan per spawn retry, surviving even the parent's exit. And
a child spawned whileend()was already draining joined a pool nothing would drain again. -
🐞 The exit backstop now tracks every child spawned and not yet seen to exit, rather than current
pool membership, so an abruptprocess.exit()during shutdown or recycling can't leave one
behind. This supersedes the PID-snapshot workaround from v17.3.1. -
🐞 A consumer-supplied
loggerthat throws can no longer strand a child: cleanup now runs before
the log, and a termination that rejects for any reason force-kills the child before it is treated
as finished. -
🐞
vacuumProcs()catches per-processend()rejections, ascloseChildProcesses()always has.
Its only caller discards the promise, so a throwingchildEndlistener became an unhandled
rejection — killing the host process and orphaning every child then mid-recycle. -
🐞 An IPC
disconnectis no longer treated as an exit. It only means the channel closed, but it
maderunning()false, which skipped both the SIGTERM and the SIGKILL. Children from an
IPC-enabled factory (stdio: [..., "ipc"]orfork()) that disconnect while running are now
terminated. -
📦 The published package no longer includes the spec suite's subprocess fixtures (
*-helper.js).
They requiredist/test.js, which was already excluded, so they were broken weight in the tarball.
Commits
- fix(ProcessPoolManager)!: harden child and task shutdown lifecycles (67046a9)
- chore: update dependencies for globals and npm-check-updates to latest versions (6bfba33)
- chore(.npmignore): add subprocess fixture exclusions for spec suite (830cbaf)
- chore(build): update Node.js version matrix to 22,24,26 in build workflow. (52397d6)
- chore: update CodeQL action versions to v4.37.4 (ddc6fb2)
- fix(ProcessTerminator): stop tests from signalling real pids (0c2aac3)
- chore(fmt) (575c02a)
- chore: update dependencies in package.json (30ee9e1)
- chore: update GitHub Actions to use latest action versions (1145fe8)
- chore: update deps (49e7057)
- chore: rename precommit npm script to all (cf100a4)