Unitech / pm2

Node.js Production Process Manager with a built-in Load Balancer.
https://pm2.keymetrics.io/docs/usage/quick-start/
Other
41.59k stars 2.62k forks source link

Cluster mode not working with yarn PnP + Express #5892

Closed gkentr closed 2 months ago

gkentr commented 2 months ago

pm2 does not seem to be managing properly the startup of 2 instances of an Express server in cluster mode. I'm using yarn PnP for package management (which shouldn't matter, I got the same with npm). My setup is:

node 20.10.0 pm2 5.4.2 yarn 4.5.0

Then to set up a vanilla Express project:

  1. yarn init
  2. Add "type": "module" and the start target "scripts": { "start": "node index.js" } in package.json
  3. Do yarn add express to install express. You should end up with something like this:

package.json:

{
  "name": "pm2-xp",
  "packageManager": "yarn@4.5.0",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.21.0"
  }
}

Then for the server (index.js):

import express from "express";

const xp = express();
const port = process.env.PORT || 3000;

const server = xp.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at: ", promise, "reason: ", reason);
});

process.on("SIGTERM", () => {
  server.close(() => {
    console.log("HTTP server closed.");
  });
});

Finally, in ecosystem.config.cjs:

module.exports = {
    apps: [
        {
            name: "pm2-xp",
            script: "yarn",
            args: "start",
            interpreter: "/bin/bash",
            instances: 2,
            exec_mode: "cluster",
            error_file: "./logs/pm2-xp_error.log",
            out_file: "./logs/pm2-xp_out.log",
            log_file: "./logs/pm2-xp.log",
            merge_logs: true,
            env: {
                NODE_ENV: "production"
            }
        }
    ]
}

Doing pm2 start ecosystem.config.cjs then results in this in ~/.pm2/pm2.log:

2024-09-17T14:18:48: PM2 log: App [pm2-xp:0] starting in -cluster mode-
2024-09-17T14:18:48: PM2 log: App [pm2-xp:0] online
2024-09-17T14:18:48: PM2 log: App [pm2-xp:1] starting in -cluster mode-
2024-09-17T14:18:48: PM2 log: App [pm2-xp:1] online
App listening on port 3000
node:events:492
      throw er; // Unhandled 'error' event
      ^

Error: listen EADDRINUSE: address already in use :::3000
    at Server.setupListenHandle [as _listen2] (node:net:1872:16)
    at listenInCluster (node:net:1920:12)
    at Server.listen (node:net:2008:7)
    at Function.listen (/Users/nemo/.yarn/berry/cache/express-npm-4.21.0-377d90d8f4-10c0.zip/node_modules/express/lib/application.js:635:24)
    at file:///Users/nemo/Projects/pm2-xp/index.js:6:19
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)
Emitted 'error' event on Server instance at:
    at emitErrorNT (node:net:1899:8)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'EADDRINUSE',
  errno: -48,
  syscall: 'listen',
  address: '::',
  port: 3000
}

I tried a couple of alternate ways to start up the cluster, like setting node as the script (providing pnp.cjs and the loader as args), but didn't find anything that works.

gkentr commented 2 months ago

After some searching I figured this out - when I was trying to run the server script via node I was getting a similar error as this issue, which led me to this post. Basically you have to run the script directly, like so:

{
    "script": "index.js",
    "interpreter_args": "--require ./.pnp.cjs --loader ./.pnp.loader.mjs"
}

In the "real" project where I have been trying to use this, part of my problem was also that the PnP script was trying to resolve modules from a global cache (which I needed to disable by setting enableGlobalCache: false in .yarnrc.yml and re-installing packages so they go into the project's local cache.

Would be nice to have something in the docs about this limitation, I imagine many people's first instinct would be to run their targets from package.json when setting this up.