forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement app clustering (github#17752)
* Install throng for easy cluster management * Extract the Express app construction into its own file * Switch server.js to use app clustering for deployed environments * Worker count is based on the lesser of process.env.WEB_CONCURRENCY and the count of CPUs * Reading clustered output is difficult, let's prefix the std{out,err} streams Co-authored-by: Jason Etcovitch <jasonetco@github.com>
- Loading branch information
1 parent
b6321bb
commit 6e20ed7
Showing
12 changed files
with
162 additions
and
41 deletions.
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const express = require('express') | ||
|
||
const app = express() | ||
require('../middleware')(app) | ||
module.exports = app |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
module.exports = function prefixStreamWrite (writableStream, prefix) { | ||
const oldWrite = writableStream.write | ||
|
||
function newWrite (...args) { | ||
const [chunk, encoding] = args | ||
|
||
// Prepend the prefix if the chunk is either a string or a Buffer. | ||
// Otherwise, just leave it alone to be safe. | ||
if (typeof chunk === 'string') { | ||
// Only prepend the prefix is the `encoding` is safe or not provided. | ||
// If it's a function, it is third arg `callback` provided as optional second | ||
if (!encoding || encoding === 'utf8' || typeof encoding === 'function') { | ||
args[0] = prefix + chunk | ||
} | ||
} else if (Buffer.isBuffer(chunk)) { | ||
args[0] = Buffer.concat([Buffer.from(prefix), chunk]) | ||
} | ||
|
||
return oldWrite.apply(this, args) | ||
} | ||
|
||
writableStream.write = newWrite | ||
|
||
return writableStream | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,116 @@ | ||
require('dotenv').config() | ||
|
||
const throng = require('throng') | ||
const os = require('os') | ||
const portUsed = require('port-used') | ||
const prefixStreamWrite = require('./lib/prefix-stream-write') | ||
|
||
// Intentionally require these for both cluster primary and workers | ||
require('./lib/check-node-version') | ||
require('./lib/handle-exceptions') | ||
require('./lib/feature-flags') | ||
|
||
const express = require('express') | ||
const portUsed = require('port-used') | ||
const warmServer = require('./lib/warm-server') | ||
const port = Number(process.env.PORT) || 4000 | ||
const app = express() | ||
|
||
require('./middleware')(app) | ||
|
||
// prevent the app from starting up during tests | ||
/* istanbul ignore next */ | ||
if (!module.parent) { | ||
// check that the development server is not already running | ||
portUsed.check(port).then(async status => { | ||
if (status === false) { | ||
// If in a deployed environment, warm the server at the start | ||
if (process.env.NODE_ENV === 'production') { | ||
// If in a true production environment, wait for the cache to be fully warmed. | ||
if (process.env.HEROKU_PRODUCTION_APP || process.env.GITHUB_ACTIONS) { | ||
await warmServer() | ||
} | ||
} | ||
|
||
// workaround for https://github.com/expressjs/express/issues/1101 | ||
const server = require('http').createServer(app) | ||
server.listen(port, () => console.log(`app running on http://localhost:${port}`)) | ||
.on('error', () => server.close()) | ||
} else { | ||
console.log(`\n\n\nPort ${port} is not available. You may already have a server running.`) | ||
console.log('Try running `killall node` to shut down all your running node processes.\n\n\n') | ||
console.log('\x07') // system 'beep' sound | ||
process.exit(1) | ||
} | ||
const { PORT, NODE_ENV } = process.env | ||
const port = Number(PORT) || 4000 | ||
|
||
function main () { | ||
// Spin up a cluster! | ||
throng({ | ||
master: setupPrimary, | ||
worker: setupWorker, | ||
count: calculateWorkerCount() | ||
}) | ||
} | ||
|
||
// Start the server! | ||
main() | ||
|
||
// This function will only be run in the primary process | ||
async function setupPrimary () { | ||
process.on('beforeExit', () => { | ||
console.log('Shutting down primary...') | ||
console.log('Exiting!') | ||
}) | ||
|
||
console.log('Starting up primary...') | ||
|
||
// Check that the development server is not already running | ||
const portInUse = await portUsed.check(port) | ||
if (portInUse) { | ||
console.log(`\n\n\nPort ${port} is not available. You may already have a server running.`) | ||
console.log('Try running `killall node` to shut down all your running node processes.\n\n\n') | ||
console.log('\x07') // system 'beep' sound | ||
process.exit(1) | ||
} | ||
} | ||
|
||
// IMPORTANT: This function will be run in a separate worker process! | ||
async function setupWorker (id, disconnect) { | ||
let exited = false | ||
|
||
// Wrap stdout and stderr to include the worker ID as a static prefix | ||
// console.log('hi') => '[worker.1]: hi' | ||
const prefix = `[worker.${id}]: ` | ||
prefixStreamWrite(process.stdout, prefix) | ||
prefixStreamWrite(process.stderr, prefix) | ||
|
||
process.on('beforeExit', () => { | ||
console.log('Exiting!') | ||
}) | ||
|
||
process.on('SIGTERM', shutdown) | ||
process.on('SIGINT', shutdown) | ||
|
||
console.log('Starting up worker...') | ||
|
||
// Load the server in each worker process and share the port via sharding | ||
const app = require('./lib/app') | ||
const warmServer = require('./lib/warm-server') | ||
|
||
// If in a deployed environment... | ||
if (NODE_ENV === 'production') { | ||
// If in a true production environment, wait for the cache to be fully warmed. | ||
if (process.env.HEROKU_PRODUCTION_APP || process.env.GITHUB_ACTIONS) { | ||
await warmServer() | ||
} | ||
} | ||
|
||
// Workaround for https://github.com/expressjs/express/issues/1101 | ||
const server = require('http').createServer(app) | ||
server | ||
.listen(port, () => console.log(`app running on http://localhost:${port}`)) | ||
.on('error', () => server.close()) | ||
|
||
function shutdown () { | ||
if (exited) return | ||
exited = true | ||
|
||
console.log('Shutting down worker...') | ||
disconnect() | ||
} | ||
} | ||
|
||
module.exports = app | ||
function calculateWorkerCount () { | ||
// Heroku's recommended WEB_CONCURRENCY count based on the WEB_MEMORY config | ||
const { WEB_CONCURRENCY } = process.env | ||
|
||
const recommendedCount = parseInt(WEB_CONCURRENCY, 10) || 1 | ||
const cpuCount = os.cpus().length | ||
|
||
// Ensure the recommended count is AT LEAST 1 for safety | ||
let workerCount = Math.max(recommendedCount, 1) | ||
|
||
// Let's do some math... | ||
// If in a deployed environment... | ||
if (NODE_ENV === 'production') { | ||
// If WEB_MEMORY or WEB_CONCURRENCY values were configured in Heroku, use | ||
// the smaller value between their recommendation vs. the CPU count | ||
if (WEB_CONCURRENCY) { | ||
workerCount = Math.min(recommendedCount, cpuCount) | ||
} else { | ||
workerCount = cpuCount | ||
} | ||
} | ||
|
||
return workerCount | ||
} |
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
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
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
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