Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds boot proxy to overcome Heroku timeouts #1

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Reaction Commerce Buildpack

This buildpack is a fork of [meteor-buildpack-horse](https://github.com/swrdfish/meteor-buildpack-horse.git) with our
modifications to ease deployment and overcome common failures (like boot timeout).

To use this with heroku:

1. Set up your app to [deploy to heroku with git](https://devcenter.heroku.com/articles/git).
2. Set this repository as the buildpack URL:

heroku buildpacks:set https://github.com/swrdfish/meteor-buildpack-horse.git
heroku buildpacks:set https://github.com/Zanobo/reaction-buildpack.git

3. Add the MongoLab addon:

heroku addons:create mongolab

4. Set the `ROOT_URL` environment variable. This is required for bundling and running the app. Either define it explicitly, or enable the [Dyno Metadata](https://devcenter.heroku.com/articles/dyno-metadata) labs addon to default to `https://<appname>.herokuapp.com`.
Expand All @@ -31,6 +34,20 @@ The following are some important environment variables for bundling and running
- `BUILDPACK_CLEAR_CACHE`: This buildpack stores the meteor installation in the [CACHE_DIR](https://devcenter.heroku.com/articles/buildpack-api#caching) to speed up subsequent builds. Set `BUILDPACK_CLEAR_CACHE=1` to clear this cache on startup.
- `BUILD_OPTIONS`: Set to any additional options you'd like to add to the invocation of `meteor build`, for example `--debug` or `--allow-incompatible-update`.

### Boot proxy

Sometimes reaction takes too much time to start and Heroku thinks that there's a problem with your app. To sidestep this
we have a simple proxy that answers every request until it's ready.

It accepts the following environment variables:

- `USE_BOOT_PROXY`: Set `USE_BOOT_PROXY=1` to enable it. It's disabled by default.
- `PING_PATH`: The route we use to test if your app is ready. Defaults to `/`. You need to add a leading slash.
- `PING_INTERVAL`: Interval between probes, in seconds. Defaults to 1 second.
- `BOOT_TIMEOUT`: Maximum time to wait for your app in seconds. Defaults to 3600 (1 Hour). If reached we exit with an
error.
- `BOOTING_URL`: Proxies transparently to this url while your app is booting if provided.

## Extras

The basic buildpack should function correctly for any normal-ish meteor app,
Expand All @@ -52,7 +69,3 @@ subdirectories. Those directories are added to `$PATH` and

So `$COMPILE_DIR/bin` etc are great places to put any extra binaries or stuff
if you need to in custom extras.

## Tips & Tricks

Please help us add tips and tricks to the [wiki](https://github.com/AdmitHub/meteor-buildpack-horse/wiki) for further help, like usage with Dokku or other environments.
163 changes: 163 additions & 0 deletions bin/boot_proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/usr/bin/env node

var spawn = require('child_process').spawn;
var http = require('http');
var httpProxy = require('http-proxy');


var USE_BOOT_PROXY = (['1', 'true', 'yes', 1].indexOf((process.env.USE_BOOT_PROXY || '').toLowerCase()) !== -1);

var PORT = process.env.PORT || 3000;
var SUBPROCESS_PORT = parseInt(process.env.SUBPROCESS_PORT) || 3030;
var PING_PATH = process.env.PING_PATH || '/';
var PING_INTERVAL = parseInt(process.env.PING_PATH) || 1;
var BOOT_TIMEOUT = parseInt(process.env.BOOT_TIMEOUT) || 60 * 60;
var BOOTING_URL = process.env.BOOTING_URL;


var ROOT_URL = process.env.ROOT_URL;
var HEROKU_APP_NAME = process.env.HEROKU_APP_NAME;

if (!ROOT_URL && HEROKU_APP_NAME) {
ROOT_URL = 'https://' + HEROKU_APP_NAME + '.herokuapp.com';
} else {
ROOT_URL = 'http://localhost';
}


var child;
function start_subprocess() {
var command = process.argv.splice(2).join(' ');
var env = Object.assign({}, process.env);

env.ROOT_URL = ROOT_URL;
if (USE_BOOT_PROXY) {
env.PORT = SUBPROCESS_PORT;
}

child = spawn(command, {
stdio: 'inherit',
shell: true,
env: env,
});


child.on('close', function (code) {
process.exit(code);
});

child.on('error', function (err) {
console.error(`Failed to run: ${command}`);
console.error(err);
process.exit(127);
});


return child;
}



start_subprocess();

if (USE_BOOT_PROXY) {
var booted = false;

var pinger = setInterval(function () {
var options = {
port: SUBPROCESS_PORT,
method: 'GET',
path: PING_PATH,
timeout: (1000*PING_INTERVAL) / 2,
};

var req = http.request(options, function (res) {
if (res.statusCode !== 200) {
return;
} else {
console.log('Application booted');
clearInterval(pinger);
booted = true;
}
});

req.on('error', function (e) {
// Silence is golden...
// console.error(`problem with request: ${e.message}`);
});
req.end();
}, 1000*PING_INTERVAL);



var bootingProxy;
var proxy = new httpProxy.createProxyServer({
target: {
port: SUBPROCESS_PORT,
},
ws: true,
});

if (BOOTING_URL) {
bootingProxy = new httpProxy.createProxyServer({
target: BOOTING_URL,
changeOrigin: true,
ws: true,
});

bootingProxy.on('error', function (err, req, res) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});

console.error(err);
res.end('There was an error');
});
}

var proxyServer = http.createServer(function (req, res) {
if (booted) {
proxy.web(req, res);
} else {
if (bootingProxy) {
bootingProxy.web(req, res);
} else {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Waiting for app to boot...');
}
}
});

proxyServer.on('upgrade', function (req, socket, head) {
if (booted) {
proxy.ws(req, socket, head);
} else {
if (bootingProxy) {
bootingProxy.ws(req, socket, head);
}
}
});

proxy.on('error', function (err, req, res) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});

console.error(err);
res.end('There was an error');
});

proxyServer.listen(PORT);

setTimeout(function () {
if (!booted) {
console.error('Application not booted after ' + BOOT_TIMEOUT + ' seconds. Quitting...');
child.kill();
process.exit(64);
}
}, BOOT_TIMEOUT*1000);
}


// Dummy interval to keep main loop busy so we don't exit early.
setInterval(function () { }, 1000)
15 changes: 13 additions & 2 deletions bin/compile
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,10 @@ fi
# assets at runtime, and thus they are not available for bundling unless meteor
# has been launched. To opt-in to this, set BUILDPACK_PRELAUNCH_METEOR=1.
if [ -n "${BUILDPACK_PRELAUNCH_METEOR+1}" ]; then
echo "-----> BUILDPACK_PRELAUNCH_METEOR: Pre-launching meteor to build packages assets"
echo "-----> BUILDPACK_PRELAUNCH_METEOR: Removing android"
# Remove the Android platform because it fails due to the Android tools not
# being installed, but leave the iOS platform because it's ignored.
HOME=$METEOR_DIR $METEOR remove-platform android || true
HOME=$METEOR_DIR timeout -s9 60 $METEOR --settings settings.json || true
fi


Expand All @@ -182,6 +181,18 @@ fi
$METEOR_NPM install reaction-cli
node_modules/.bin/reaction plugins load

if [ -n "${BUILDPACK_PRELAUNCH_METEOR+1}" ]; then
echo "-----> BUILDPACK_PRELAUNCH_METEOR: Pre-launching reaction to build packages assets"
HOME=$METEOR_DIR timeout -s9 600 node_modules/.bin/reaction run || true
fi


#
# Install our boot proxy
#

$METEOR_NPM install --save http-proxy
cp "$BUILDPACK_DIR/bin/boot_proxy.js" "$COMPILE_DIR/bin"

# Now on to bundling. Don't put the bundle in $APP_CHECKOUT_DIR during
# bundling, or it will recurse, trying to bundle up its own bundling.
Expand Down
2 changes: 1 addition & 1 deletion bin/release
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ cat <<EOF
addons:
- mongolab
default_process_types:
web: .meteor/heroku_build/bin/node .meteor/heroku_build/app/main.js
web: .meteor/heroku_build/bin/node .meteor/heroku_build/bin/boot_proxy.js .meteor/heroku_build/bin/node .meteor/heroku_build/app/main.js
EOF