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

worker_threads not respecting --require in process.execArgv or process.env.NODE_OPTIONS #37410

Open
clemyan opened this issue Feb 17, 2021 · 4 comments
Labels
worker Issues and PRs related to Worker support.

Comments

@clemyan
Copy link

clemyan commented Feb 17, 2021

  • Versions:
    • v12.20.2
    • v14.15.5
    • v15.8.0
  • Platforms:
    • CentOS 7 (Linux 3.10.0-1127.8.2.el7.x86_64 #1 SMP Tue May 12 16:57:42 UTC 2020 x86_64 GNU/Linux)
    • Windows 10 (Microsoft Windows NT 10.0.19041.0 x64)
  • Subsystem: worker_threads

What steps will reproduce the bug?

Mutate either process.env.NODE_OPTIONS or process.execArgv to include a --require flag before spawning a worker thread.

Test script
const { Worker, isMainThread, SHARE_ENV } = require('worker_threads')
const { once } = require('events')

if (module.parent && module.parent.id === 'internal/preload') {
  // Preload
  delete require.cache[__filename]
  console.log('preloaded')
} else if (isMainThread && !process.send) {
  // Main
  const tests = [
    async () => {
      console.log('worker process.execArgv')
      const orig = process.execArgv
      process.execArgv = ['--require', __filename, ...process.execArgv]
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.execArgv = orig
    },
    async () => {
      console.log('worker process.env.NO')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('worker SHARE_ENV')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename, { env: SHARE_ENV })
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
  ]
  ;(async () => {
    for (const f of tests) {
      await f()
      console.log()
    }
  })()
}

What is the expected behavior?

The --require flag is respected in each case:

worker process.execArgv
preloaded

worker process.env.NO
preloaded

worker SHARE_ENV
preloaded

What do you see instead?

The --require flag is not respected:

worker process.execArgv

worker process.env.NO

worker SHARE_ENV

Additional information

tl;dr: Seems like the default value of env and execArgv options of the Worker constructor is the initial value of process.env and process.execArgv instead of the current value. This behavior is in contrast to child_process.fork.

Full test script

This is the full test script with all the tests I have done (detailed below).

Test script
const { Worker, isMainThread, SHARE_ENV, parentPort } = require('worker_threads')
const { fork } = require('child_process')
const { once } = require('events')

if (module.parent && module.parent.id === 'internal/preload') {
  // Preload
  delete require.cache[__filename]
  console.log('preloaded')
} else if (isMainThread && !process.send) {
  // Main
  const tests = [
    async () => {
      console.log('worker control')
      const worker = new Worker(__filename)
      await once(worker, 'exit')
    },
    async () => {
      console.log('worker process.execArgv')
      const orig = process.execArgv
      process.execArgv = ['--require', __filename, ...process.execArgv]
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.execArgv = orig
    },
    async () => {
      console.log('worker execArgv option')
      const worker = new Worker(__filename, { execArgv: ['--require', __filename, ...process.execArgv] })
      await once(worker, 'exit')
    },
    async () => {
      console.log('worker process.env.NO')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('worker SHARE_ENV')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename, { env: SHARE_ENV })
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('worker env option')
      const worker = new Worker(__filename, { env: { NODE_OPTIONS: `--require ${__filename} ${process.env.NODE_OPTIONS}` } })
      await once(worker, 'exit')
    },

    async () => {
      console.log('fork control')
      const cp = fork(__filename)
      await once(cp, 'exit')
    },
    async () => {
      console.log('fork process.execArgv')
      const orig = process.execArgv
      process.execArgv = ['--require', __filename, ...process.execArgv]
      const cp = fork(__filename)
      await once(cp, 'exit')
      process.execArgv = orig
    },
    async () => {
      console.log('fork execArgv option')
      const cp = fork(__filename, { execArgv: ['--require', __filename, ...process.execArgv] })
      await once(cp, 'exit')
    },
    async () => {
      console.log('fork process.env.NO')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const cp = fork(__filename)
      await once(cp, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('fork env option')
      const cp = fork(__filename, { env: { NODE_OPTIONS: `--require ${__filename} ${process.env.NODE_OPTIONS}` } })
      await once(cp, 'exit')
    },
  ]
  ;(async () => {
    for (const f of tests) {
      await f()
      console.log()
    }
  })()
}
Expected output
worker control

worker process.execArgv
preloaded

worker execArgv option
preloaded

worker process.env.NO
preloaded

worker SHARE_ENV
preloaded

worker env option
preloaded

fork control

fork process.execArgv
preloaded

fork execArgv option
preloaded

fork process.env.NO
preloaded

fork env option
preloaded
Actual output
worker control

worker process.execArgv

worker execArgv option
preloaded

worker process.env.NO

worker SHARE_ENV

worker env option
preloaded

fork control

fork process.execArgv
preloaded

fork execArgv option
preloaded

fork process.env.NO
preloaded

fork env option
preloaded

Running node with --require affects worker threads

Modify the test script to use the following test case

async () => {
  console.log('worker control')
  const worker = new Worker(__filename)
  await once(worker, 'exit')
}

Then run the script with node --require ./main.js main.js outputs:

preloaded
worker control
preloaded

which shows the --require flag is respected in both the main thread and the worker thread

--require in Worker options is respected

When explicitly passing execArgv or env.NODE_OPTIONS to the Worker constructor options, the --require flag is respected:

async () => {
  console.log('worker execArgv option')
  const worker = new Worker(__filename, { execArgv: ['--require', __filename, ...process.execArgv] })
  await once(worker, 'exit')
},
async () => {
  console.log('worker env option')
  const worker = new Worker(__filename, { env: { NODE_OPTIONS: `--require ${__filename} ${process.env.NODE_OPTIONS}` } })
  await once(worker, 'exit')
},

The above logs:

worker execArgv option
preloaded

worker env option
preloaded

child_process.fork respects --require in process.execArgv or process.env.NODE_OPTIONS

const { fork } = require('child_process')

async () => {
  console.log('fork process.execArgv')
  const orig = process.execArgv
  process.execArgv = ['--require', __filename, ...process.execArgv]
  const cp = fork(__filename)
  await once(cp, 'exit')
  process.execArgv = orig
},
async () => {
  console.log('fork process.env.NO')
  const orig = process.env.NODE_OPTIONS
  process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
  const cp = fork(__filename)
  await once(cp, 'exit')
  process.env.NODE_OPTIONS = orig
},

The above logs:

fork process.execArgv
preloaded

fork process.env.NO
preloaded
@aduh95 aduh95 added the worker Issues and PRs related to Worker support. label Feb 17, 2021
@aduh95
Copy link
Contributor

aduh95 commented Feb 17, 2021

Related? #36531

@clemyan
Copy link
Author

clemyan commented Feb 17, 2021

Not really. As far as I understand the comment thread, #36531 occurs because --require passed to the node process is respected by workers (which is intended and expected). This issue is about how --require is not respected (by default) by workers if not passed to the node process.

In order words:

--require flag is ... new Worker cp.fork
... passed to node process (e.g. node -r ... ) Respected Respected
... added to process.execArgv or process.env Not respected Respected
... passed explicitly to Worker/fork Respected Respected

#36531 is about the top-left "Respected" cell. This issue is about the "Not respected" cell.

@aduh95
Copy link
Contributor

aduh95 commented Feb 17, 2021

//cc @nodejs/workers

@clemyan
Copy link
Author

clemyan commented Feb 18, 2021

A little more investigation.

The discrepancy between Worker vs cp.fork is probably because the default values of execArgv and env are resolved in JS for cp.fork (here and here) while they are resolved in C++ for Worker (here and here).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
worker Issues and PRs related to Worker support.
Projects
None yet
Development

No branches or pull requests

2 participants