Skip to content

Instantly share code, notes, and snippets.

@joshbetz
Last active July 3, 2022 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshbetz/59c4e6af9312e6bf162707f377ae4f4c to your computer and use it in GitHub Desktop.
Save joshbetz/59c4e6af9312e6bf162707f377ae4f4c to your computer and use it in GitHub Desktop.
Node cluster example
#!/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 );
}
/**
* 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