Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error: write EPIPE - trying to write into terminating child_process #84

Closed
AriPerkkio opened this issue Apr 11, 2024 · 0 comments · Fixed by #85
Closed

Error: write EPIPE - trying to write into terminating child_process #84

AriPerkkio opened this issue Apr 11, 2024 · 0 comments · Fixed by #85

Comments

@AriPerkkio
Copy link
Member

AriPerkkio commented Apr 11, 2024

Tinypool throws Error: write EPIPE errors randomly on Vitest when running under heavy load. I can reproduce this issue 3/10 times on private pnpm monorepo with 3 packages running vitest@1.3.0. Using stress unix command helps reproducing the issue.

Minimal reproduction

This is caused by race condition so reproducing is flaky. I'm able to reproduce this quite easily locally.

import Tinypool from './dist/esm/index.js'

const messages = []
const listeners = []
let setRunning
const isRunning = new Promise((r) => (setRunning = r))
const channel = {
  onMessage: (listener) => listeners.push(listener),
  postMessage: (message) => {
    if (message === 'WORKER_START') setRunning()
    else messages.push(message)
  },
}

const pool = new Tinypool({
  runtime: 'child_process',
  minThreads: 10,
  maxThreads: 10,
  filename: new URL('./worker.mjs', import.meta.url).href,
})

pool.run({}, { channel })

await isRunning
listeners.forEach((listener) => listener('Hello from main thread'))

pool.destroy()
listeners.forEach((listener) => listener('Hello from main thread')) // <-- Key part is here

console.log('Done', messages)
export default async function run() {
  process.send('WORKER_START')

  return new Promise((resolve) => {
    process.on('message', (message) => {
      // Ignore Tinypool's internal messages
      if (message?.__tinypool_worker_message__) return

      process.send({ received: message, response: 'Hello from worker' })
      resolve()
    })
  })
}
ari ~/Git/tinypool (fix/prevent-write-to-terminating-worker) $ node example.mjs
Done [
  { received: 'Hello from main thread', response: 'Hello from worker' }
]
Done [
  { received: 'Hello from main thread', response: 'Hello from worker' }
]
Done [
  { received: 'Hello from main thread', response: 'Hello from worker' }
]
node:events:496
      throw er; // Unhandled 'error' event
      ^

Error: write EPIPE
    at target._send (node:internal/child_process:879:20)
    at target.send (node:internal/child_process:752:19)
    at ProcessWorker.send (file:///xyz/tinypool/dist/esm/index.js:218:18)
    at file:///xyz/tinypool/dist/esm/index.js:212:12
    at file:///xyz/tinypool/example.mjs:29:33
    at Array.forEach (<anonymous>)
    at file:///xyz/tinypool/example.mjs:29:11
Emitted 'error' event on Tinypool instance at:
    at EventEmitterReferencingAsyncResource.runInAsyncScope (node:async_hooks:206:9)
    at Tinypool.emit (file:///xyz/tinypool/dist/esm/index.js:61:31)
    at file:///xyz/tinypool/dist/esm/index.js:752:30
    at ChildProcess.<anonymous> (file:///xyz/tinypool/dist/esm/index.js:242:16)
    at ChildProcess.emit (node:events:530:35)
    at node:internal/child_process:883:39
    at process.processTicksAndRejections (node:internal/process/task_queues:77:11) {
  errno: -32,
  code: 'EPIPE',
  syscall: 'write'
}

Error is coming from here:

// Mirror channel's messages to process
this.channel.onMessage((message: any) => {
this.process.send(message)
})

Vitest's channel is simple EventEmitter: https://github.com/vitest-dev/vitest/blob/fc26f562a7faf03b9b8810b6ba374346364a9259/packages/vitest/src/node/pools/forks.ts#L20-L23. The erroneous message seems to be file transform request which resolves after tests have passed and worker is closing (cc. @sheremet-va). The this.process.connected returns true and this.process.channel is still defined when the error happens. 🤷

As work-around we can keep track of if this.terminate has been called and ignore all channel's messages after that.

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 a pull request may close this issue.

1 participant