diff --git a/package.json b/package.json index 39837d5..52a638a 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "license": "MIT", "scripts": { "dev": "tsx --watch ./example/index.ts", - "test": "bun run test:node", + "test": "bun run test:node && bun run test:node-cluster", "test:node": "npm install --prefix ./test/node/cjs/ && npm install --prefix ./test/node/esm/ && node ./test/node/cjs/index.js && node ./test/node/esm/index.js", + "test:node-cluster": "node ./test/cluster/cluster.cjs port && node ./test/cluster/cluster.cjs object && node ./test/cluster/cluster.cjs true && node ./test/cluster/cluster.cjs false", "build": "bun build.ts", "release": "npm run build && npm run test && npm publish --access public" }, diff --git a/src/index.ts b/src/index.ts index a69b286..e2a4d65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -107,9 +107,11 @@ export const node = () => { port: options, silent: true, websocket, - fetch: app.fetch + fetch: app.fetch, + reusePort: true } : { + reusePort: true, ...options, silent: true, websocket, diff --git a/test/cluster/cluster.cjs b/test/cluster/cluster.cjs new file mode 100644 index 0000000..454fd1e --- /dev/null +++ b/test/cluster/cluster.cjs @@ -0,0 +1,89 @@ +const cluster = require('node:cluster') +const { Elysia } = require('elysia') +const { exit, pid } = require('node:process') +const { node } = require('@elysiajs/node') + +const workersAmount = 5 +const port = 3000 +// Use args to differentiate parameter +// So code is more compact +const arg2 = process.argv[2] +let parameter +if (arg2 === 'port') { + parameter = port +} else if (arg2 === 'object') { + parameter = { + port + } +} else if (arg2 === 'true') { + parameter = { + port, + reusePort: true + } +} else if (arg2 === 'false') { + parameter = { + port, + reusePort: false + } +} else { + console.error(`Unknown argument: ${arg2}. Expected: port, object, true, false`) + exit(1) +} + +/** + * Method to stop workers and then exit the current program + * @param {Worker[]} workers list of workers + * @param {number} code exit code + */ +function shutdown(workers, code) { + workers.forEach((it) => { + it.kill() + }) + exit(code) +} + +/** + * Start primary node + * This will create workers and then send request to primary which will then + * spread the requests to the workers. After that it will check for the result. + */ +async function startPrimary() { + let workers = [] + for (let i = 0; i < workersAmount; i++) { + workers.push(cluster.fork()) + } + // we need some delay to allow Elysia to initialize + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // Make n API calls, we should receive n different PIDs back + // Checking if a server is run can only be done this way at the moment + // because error is really deep in srvx and async + // Even callback in Elysia.listen will be still be run even on error + const promises = workers.map(async (it) => { + const result = await fetch(`http://localhost:${port}`) + const workerPid = await result.text() + return workerPid + }) + const result = await Promise.all(promises) + const pidsCount = new Set(result).size; + if (arg2 === 'false') { + if (pidsCount !== 1) { + console.error('❌ Server should return 1 pid.') + shutdown(workers, 1) + } + console.log('✅ Test exclusive mode succeed!') + shutdown(workers, 0) + } + if (pidsCount !== workersAmount) { + console.error("❌ Clustering error, number of pids doesn't match.") + shutdown(workers, 1) + } + console.log('✅ Test cluster mode succeed!') + shutdown(workers, 0) +} + +if (cluster.isPrimary) { + startPrimary() +} else { + new Elysia({ adapter: node() }).get(`/`, pid).listen(parameter) +}