Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ interface Options {
isolateWorkers?: boolean
teardown?: string
serialization?: SerializationType
/**
* When `true`, the pool does not surface an `error` event for a worker
* that exits while it has no in-flight task. This is useful for pools
* that already track worker health via their own channel (e.g. cloud
* workerd subprocesses that may segfault during teardown — see
* https://github.com/cloudflare/workerd/issues/6763) and do not want
* a post-task uncaught exception to fail the whole pool run.
*
* Defaults to `false` to preserve the historic behavior of surfacing
* post-task uncaught exceptions via the pool's `error` event.
*/
allowWorkerIdleExit?: boolean
}

interface FilledOptions extends Options {
Expand All @@ -168,6 +180,7 @@ interface FilledOptions extends Options {
concurrentTasksPerWorker: number
useAtomics: boolean
taskQueue: TaskQueue
allowWorkerIdleExit: boolean
}

const kDefaultOptions: FilledOptions = {
Expand All @@ -182,6 +195,7 @@ const kDefaultOptions: FilledOptions = {
useAtomics: true,
taskQueue: new ArrayTaskQueue(),
trackUnmanagedFds: true,
allowWorkerIdleExit: false,
}

interface RunOptions {
Expand Down Expand Up @@ -837,7 +851,14 @@ class ThreadPool {
for (const taskInfo of taskInfos) {
taskInfo.done(err, null)
}
} else {
} else if (!this.options.allowWorkerIdleExit) {
// Worker exited while idle (no in-flight task). By default this is
// surfaced as a pool-level `error` event so post-task uncaught
// exceptions aren't silently swallowed. Pools that track worker
// health via their own channel (e.g. cloudflare/vitest-pool-workers
// running workerd, which can segfault on shutdown — see
// https://github.com/cloudflare/workerd/issues/6763) can opt out
// via `allowWorkerIdleExit: true`.
this.publicInterface.emit('error', err)
}
})
Expand Down
58 changes: 58 additions & 0 deletions test/allow-worker-idle-exit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { dirname, resolve } from 'node:path'
import { Tinypool } from 'tinypool'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))

test('uncaught exception after task yields error event (default)', async () => {
const pool = new Tinypool({
filename: resolve(__dirname, 'fixtures/eval.js'),
maxThreads: 1,
useAtomics: false,
})

let errorEmitted: Error | undefined
pool.on('error', (err) => {
errorEmitted = err as Error
})

const taskResult = pool.run(`
setTimeout(() => { throw new Error("not_caught") }, 200);
42
`)
expect(await taskResult).toBe(42)

// Wait long enough for the async throw + worker exit to propagate.
await new Promise((r) => setTimeout(r, 600))

expect(errorEmitted?.message).toEqual('not_caught')

await pool.destroy()
})

test('uncaught exception after task does NOT yield error event when allowWorkerIdleExit is true', async () => {
const pool = new Tinypool({
filename: resolve(__dirname, 'fixtures/eval.js'),
maxThreads: 1,
useAtomics: false,
allowWorkerIdleExit: true,
})

const errors: unknown[] = []
pool.on('error', (err) => {
errors.push(err)
})

const taskResult = pool.run(`
setTimeout(() => { throw new Error("ignored_when_idle") }, 200);
42
`)
expect(await taskResult).toBe(42)

// Wait long enough for the async throw + worker exit to propagate.
await new Promise((r) => setTimeout(r, 600))

expect(errors).toEqual([])

await pool.destroy()
})