diff --git a/Procfile b/Procfile index e8bdf97fa9e..320f6aa1ca2 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ release: bin/diesel migration run -web: bin/start-nginx ./target/release/server +web: ./script/start-web.sh background_worker: ./target/release/background-worker diff --git a/config/environment.js b/config/environment.js index 7681acac190..8d03ba05410 100644 --- a/config/environment.js +++ b/config/environment.js @@ -22,6 +22,10 @@ module.exports = function(environment) { // Here you can pass flags/options to your application instance // when it is created }, + + fastboot: { + hostWhitelist: ['crates.io', /^localhost:\d+$/, /\.herokuapp\.com$/], + }, }; if (environment === 'development') { diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index 8f15875f3c9..c12f5336bcd 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -64,6 +64,13 @@ http { proxy_pass http://app_server; } + <% if ENV['USE_FASTBOOT'] %> + # Just in case, only forward "/policies" to Ember for a moment + location = /policies { + proxy_pass http://localhost:9000; + } + <% end %> + location ~ ^/api/v./crates/new$ { proxy_pass http://app_server; diff --git a/fastboot.js b/fastboot.js new file mode 100644 index 00000000000..5c50b9b7888 --- /dev/null +++ b/fastboot.js @@ -0,0 +1,63 @@ +/* eslint-disable no-console */ +/* eslint-env node */ + +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const FastBootAppServer = require('fastboot-app-server'); + +// because fastboot-app-server uses cluster, but it might change in future +const cluster = require('cluster'); + +class LoggerWithoutTimestamp { + constructor() { + this.prefix = cluster.isMaster ? 'master' : 'worker'; + } + writeLine() { + this._write('info', Array.prototype.slice.apply(arguments)); + } + + writeError() { + this._write('error', Array.prototype.slice.apply(arguments)); + } + + _write(level, args) { + args[0] = `[${level}][${this.prefix}] ${args[0]}`; + console.log.apply(console, args); + } +} + +function writeAppInitializedWhenReady(logger) { + let timeout; + + timeout = setInterval(function() { + logger.writeLine('waiting backend'); + if (fs.existsSync('/tmp/backend-initialized')) { + logger.writeLine('backend is up. let heroku know the app is ready'); + fs.writeFileSync('/tmp/app-initialized', 'hello'); + clearInterval(timeout); + } else { + logger.writeLine('backend is still not up'); + } + }, 1000); +} + +var logger = new LoggerWithoutTimestamp(); + +logger.writeLine(`${os.cpus().length} cores available`); + +let workerCount = process.env.WEB_CONCURRENCY || 1; + +let server = new FastBootAppServer({ + distPath: 'dist', + port: 9000, + ui: logger, + workerCount: workerCount, +}); + +if (!cluster.isWorker) { + writeAppInitializedWhenReady(logger); +} + +server.start(); diff --git a/fastboot/initializers/ajax.js b/fastboot/initializers/ajax.js new file mode 100644 index 00000000000..f0950ec2caf --- /dev/null +++ b/fastboot/initializers/ajax.js @@ -0,0 +1,8 @@ +export default { + name: 'ajax-service', + initialize() { + // This is to override Fastboot's initializer which prevents ember-fetch from working + // https://github.com/ember-fastboot/ember-cli-fastboot/blob/master/fastboot/initializers/ajax.js + // https://github.com/ember-cli/ember-fetch#ajax-service + }, +}; diff --git a/package.json b/package.json index 7bd40819da3..698347610a7 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "lint:js": "eslint . --cache", "lint:deps": "ember dependency-lint", "prettier": "prettier --write '{app,tests,mirage}/**/*.js'", - "start": "FASTBOOT_DISABLED=true ember serve", - "start:live": "FASTBOOT_DISABLED=true ember serve --proxy https://crates.io", - "start:local": "FASTBOOT_DISABLED=true ember serve --proxy http://127.0.0.1:8888", - "start:staging": "FASTBOOT_DISABLED=true ember serve --proxy https://staging-crates-io.herokuapp.com", + "start": "./script/ember.sh serve", + "start:live": "./script/ember.sh serve --proxy https://crates.io", + "start:local": "./script/ember.sh serve --proxy http://127.0.0.1:8888", + "start:staging": "./script/ember.sh serve --proxy https://staging-crates-io.herokuapp.com", "test": "ember exam --split=2 --parallel", "test-coverage": "COVERAGE=true npm run test && ember coverage-merge && rm -rf coverage_* coverage/coverage-summary.json && nyc report" }, diff --git a/script/ember.sh b/script/ember.sh new file mode 100755 index 00000000000..b67e00cabdb --- /dev/null +++ b/script/ember.sh @@ -0,0 +1,12 @@ +#! /bin/sh +set -ue + +export FASTBOOT_DISABLED + +if [ "${USE_FASTBOOT:-0}" = '1' ]; then + unset FASTBOOT_DISABLED +else + FASTBOOT_DISABLED=1 +fi + +ember "$@" diff --git a/script/start-web.sh b/script/start-web.sh new file mode 100755 index 00000000000..47317d0867c --- /dev/null +++ b/script/start-web.sh @@ -0,0 +1,12 @@ +#! /bin/bash +set -ue + +if [[ "${USE_FASTBOOT:-0}" = 1 ]]; then + export USE_FASTBOOT=1 + node --optimize_for_size --max_old_space_size=200 fastboot.js & + bin/start-nginx ./target/release/server & + wait -n +else + unset USE_FASTBOOT + bin/start-nginx ./target/release/server +fi diff --git a/src/bin/server.rs b/src/bin/server.rs index 73bfd7d422c..3871f9f6c53 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -39,6 +39,8 @@ fn main() { boot::categories::sync(categories_toml).unwrap(); let heroku = dotenv::var("HEROKU").is_ok(); + let fastboot = dotenv::var("USE_FASTBOOT").is_ok(); + let port = if heroku { 8888 } else { @@ -102,8 +104,13 @@ fn main() { // Creating this file tells heroku to tell nginx that the application is ready // to receive traffic. if heroku { - println!("Writing to /tmp/app-initialized"); - File::create("/tmp/app-initialized").unwrap(); + let path = if fastboot { + "/tmp/backend-initialized" + } else { + "/tmp/app-initialized" + }; + println!("Writing to {}", path); + File::create(path).unwrap(); } // Block the main thread until the server has shutdown