Last active
May 31, 2024 14:25
-
-
Save joshbetz/59c4e6af9312e6bf162707f377ae4f4c to your computer and use it in GitHub Desktop.
Node cluster example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/** | |
* External dependencies | |
*/ | |
const cluster = require( 'cluster' ); | |
const CPUs = require( 'os' ).cpus().length; | |
// Process config | |
const PROC_TITLE = 'node-server'; | |
const SHUTDOWN_TIMEOUT = process.env.SHUTDOWN_TIMEOUT || 5000; | |
const SOCKET_TIMEOUT = process.env.SOCKET_TIMEOUT || 60000; | |
const REQUEST_TIMEOUT = process.env.REQUEST_TIMEOUT || 30000; | |
let WORKERS = process.env.WORKERS || CPUs; | |
if ( cluster.isPrimary ) { | |
process.title = PROC_TITLE; | |
console.log( 'Starting %s (main server) process (pid %d)', process.title, process.pid ); | |
if ( WORKERS > CPUs ) { | |
console.log( 'Invalid number of workers %d for %d CPUs, starting %d workers', WORKERS, CPUs, CPUs ); | |
WORKERS = CPUs; | |
} | |
for ( let i = 0; i < WORKERS; i++ ) { | |
cluster.fork(); | |
} | |
cluster.on( 'listening', ( worker, address ) => { | |
console.log( 'Worker %d (pid %d) listening on http://%s:%d', | |
worker.id, | |
worker.process.pid, | |
address.address || '127.0.0.1', | |
address.port | |
); | |
} ); | |
cluster.on( 'exit', ( worker, code, signal ) => { | |
// If a worker dies in a non-graceful way, restart it | |
if ( ! worker.exitedAfterDisconnect ) { | |
console.log( 'Worker %d (pid %d) died with code %d and signal %s, restarting', worker.id, worker.process.pid, code, signal ); | |
cluster.fork(); | |
return; | |
} | |
if ( code !== 0 ) { | |
console.log( 'Worker %d shut down with code %d', worker.id, code ); | |
} | |
} ); | |
const gracefulShutdown = worker => { | |
const shutdown = setTimeout( () => { | |
// Force shutdown after timeout | |
worker.kill(); | |
}, SHUTDOWN_TIMEOUT ); | |
worker.once( 'exit', () => clearTimeout( shutdown ) ); | |
worker.disconnect(); | |
} | |
// Graceful reload workers without restarting main process | |
process.on( 'SIGHUP', () => { | |
console.log( 'Caught SIGHUP, reloading workers' ); | |
for ( const id in cluster.workers ) { | |
cluster.fork().on( 'listening', () => { | |
gracefulShutdown( cluster.workers[ id ] ); | |
} ) | |
} | |
} ); | |
// Graceful shutdown | |
process.on( 'SIGINT', () => { | |
console.log( 'Caught SIGINT, initiating graceful shutdown' ); | |
for ( const id in cluster.workers ) { | |
gracefulShutdown( cluster.workers[ id ] ); | |
} | |
} ); | |
} else { | |
process.title = `${PROC_TITLE}.worker.${cluster.worker.id}`; | |
const server = require( './server' ); | |
// Socket timeout will terminate the connection with an empty response. | |
// You probably want to handle this separately with an HTTP reponse. | |
server.timeout = SOCKET_TIMEOUT; | |
server.on( 'timeout', socket => { | |
console.log( 'socket timeout' ); | |
} ); | |
// Timeout http request after REQUEST_TIMEOUT | |
server.on( 'request', ( req, res ) => { | |
const timeout = setTimeout( () => { | |
console.log( 'request timeout' ); | |
res.writeHead( 408, { 'Content-Type': 'text/plain' } ); | |
res.end( 'Request Timeout' ); | |
}, REQUEST_TIMEOUT ); | |
res.on( 'close', () => clearTimeout( timeout ) ); | |
} ); | |
// Graceful shutdown | |
process.on( 'disconnect', () => { | |
server.close( () => process.exit( 0 ) ); | |
} ); | |
process.on( 'SIGTERM', () => { | |
// Immediately exit on worker.kill() | |
process.exit( 1 ); | |
} ); | |
process.on( 'SIGINT', () => { | |
// Ignore first SIGINT propagated from parent | |
// Immediately shutdown on second SIGINT | |
process.on( 'SIGINT', () => { | |
process.exit( 1 ); | |
} ); | |
} ); | |
// Bail out if we get an uncaught exception | |
// Who knows what bad state is left behind | |
// Creating a new process will give us a clean slate | |
const unexpectedErrorHandler = error => { | |
console.log( error.stack ); | |
// Exit immediately, no need to wait for graceful shutdown | |
process.exit( 1 ); | |
}; | |
process.on( 'uncaughtException', unexpectedErrorHandler ); | |
process.on( 'unhandledRejection', unexpectedErrorHandler ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* External dependencies | |
*/ | |
const { createServer } = require( 'http' ); | |
const sleep = async time => { | |
return new Promise( resolve => { | |
setTimeout( resolve, time ); | |
} ); | |
}; | |
function randomInteger(min, max) { | |
return Math.floor( Math.random() * ( max - min + 1 ) ) + min; | |
} | |
module.exports = createServer( async ( req, res ) => { | |
if ( Math.random() > 0.999 ) { | |
// Randomly throws an uncaught error 0.1% of the time | |
throw Error( '0.1% error' ); | |
} | |
// do something for random time bewteen 50 - 250ms | |
await sleep( randomInteger( 50, 250 ) ); | |
res.end( 'Hello World\n' ); | |
} ).listen( process.env.port || 3000 ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment