oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
74.11k stars 2.77k forks source link

Bun.serve and node:cluster don't round-robin http requests when listening on unix domain socket #13611

Open bompus opened 2 months ago

bompus commented 2 months ago

What version of Bun is running?

1.1.26+0a37423ba

What platform is your computer?

Linux 6.10.6-x64v3-xanmod1 x86_64 x86_64

What steps can reproduce the bug?

// bun-serve-spawn-cluster.js
import { spawn } from "bun";

const method = process.argv[2];
if (['port', 'socket'].includes(method) !== true) {
  console.error(`usage: bun ${process.argv[1]} [port,socket]`);
  process.exit(1);
}

const cpus = 4; // Number of CPU cores
const buns = new Array(cpus);

for (let i = 0; i < cpus; i++) {
  buns[i] = spawn({
    cmd: ["bun", "./bun-serve-worker.js", method, i],
    stdout: "inherit",
    stderr: "inherit",
    stdin: "inherit",
  });
}

function kill() {
  for (const bun of buns) {
    bun.kill();
  }
}

process.on("SIGINT", kill);
process.on("exit", kill);
// bun-serve-worker.js
import { serve } from "bun";

const method = process.argv[2]; // port or socket
const id = process.argv[3] ?? process.env.WORKER_NUM; // worker id

if (['port', 'socket'].includes(method) !== true || id === undefined) {
  console.error(`usage: bun ${process.argv[1]} [port,socket] [1,2,3,4]`);
  process.exit(1);
}

const serveOpts = {
  // hostname: '127.0.0.1',
  // port: 8080,
  // unix: '/run/bun-serve-worker.sock',
  development: false,

  // Share the same port across multiple processes
  // This is the important part!
  reusePort: true,

  async fetch(request) {
    return new Response("Hello from Bun #" + id + "!\n");
  }
};

if (method === 'port') {
  serveOpts.hostname = '127.0.0.1';
  serveOpts.port = 8080;
  console.log(`bun-server-worker [${id}] test via: curl http://127.0.0.1:8080`);
} else if (method === 'socket') {
  serveOpts.unix = '/run/bun-serve-worker.sock';
  console.log(`bun-server-worker [${id}] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080`);
}

serve(serveOpts);

Terminal 1: $ bun bun-serve-spawn-cluster.js port Terminal 2: $ for x in 1 2 3 4 5 6 7 8; do curl http://127.0.0.1:8080; done Terminal 2: notice that different workers respond to the request Terminal 1: press CTRL+C to kill

Terminal 1: $ bun bun-serve-spawn-cluster.js socket Terminal 2: $ for x in 1 2 3 4 5 6 7 8; do curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080; done Terminal 2: notice that only one worker responds to the request Terminal 1: press CTRL+C to kill

In Terminal 1, you can also test out bun-serve-node-cluster.js, which uses the node:cluster module instead, which exhibits the same behavior.

// bun-serve-node-cluster.js
import cluster from 'node:cluster';

const method = process.argv[2];
if (['port', 'socket'].includes(method) !== true) {
  console.error(`usage: bun ${process.argv[1]} [port,socket]`);
  process.exit(1);
}

const cpus = 4; // Number of CPU cores

cluster.setupPrimary({
  exec: './bun-serve-worker.js',
  args: [method]
});

for (let i = 0; i < cpus; i++) {
  cluster.fork({ WORKER_NUM: i });
}

What is the expected behavior?

Regardless of listening on a tcp port or unix domain socket, it should round-robin (or at least distribute somehow) the requests, similar to the following:

bun-server-worker [2] test via: curl http://127.0.0.1:8080
bun-server-worker [3] test via: curl http://127.0.0.1:8080
bun-server-worker [0] test via: curl http://127.0.0.1:8080
bun-server-worker [1] test via: curl http://127.0.0.1:8080
Hello from Bun #3!
Hello from Bun #1!
Hello from Bun #0!
Hello from Bun #0!
Hello from Bun #2!
Hello from Bun #1!
Hello from Bun #2!
Hello from Bun #3!

What do you see instead?

When listening on a unix domain socket, only one worker, likely the last one executed during the spawn race, answers all of the requests:

bun-server-worker [1] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
bun-server-worker [0] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
bun-server-worker [3] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
bun-server-worker [2] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!

Additional information

I consider this a bug, because Node.js works as expected ( distributes the requests to workers, regardless of listening on tcp port or unix domain socket) when using node:cluster.

Jarred-Sumner commented 2 months ago

oh I wonder how SO_REUSEPORT is supposed to work with unix domain sockets