diff options
Diffstat (limited to 'samples/CoffeeScript/rack_application.coffee')
| -rw-r--r-- | samples/CoffeeScript/rack_application.coffee | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/samples/CoffeeScript/rack_application.coffee b/samples/CoffeeScript/rack_application.coffee new file mode 100644 index 0000000..674313b --- /dev/null +++ b/samples/CoffeeScript/rack_application.coffee @@ -0,0 +1,256 @@ +# The `RackApplication` class is responsible for managing a +# [Nack](http://josh.github.com/nack/) pool for a given Rack +# application. Incoming HTTP requests are dispatched to +# `RackApplication` instances by an `HttpServer`, where they are +# subsequently handled by a pool of Nack worker processes. By default, +# Pow tells Nack to use a maximum of two worker processes per +# application, but this can be overridden with the configuration's +# `workers` option. +# +# Before creating the Nack pool, Pow executes the `.powrc` and +# `.powenv` scripts if they're present in the application root, +# captures their environment variables, and passes them along to the +# Nack worker processes. This lets you modify your `RUBYOPT` to use +# different Ruby options, for example. +# +# If [rvm](http://rvm.beginrescueend.com/) is installed and an +# `.rvmrc` file is present in the application's root, Pow will load +# both before creating the Nack pool. This makes it easy to run an +# app with a specific version of Ruby. +# +# Nack workers remain running until they're killed, restarted (by +# touching the `tmp/restart.txt` file in the application root), or +# until the application has not served requests for the length of time +# specified in the configuration's `timeout` option (15 minutes by +# default). + +async = require "async" +fs = require "fs" +nack = require "nack" + +{bufferLines, pause, sourceScriptEnv} = require "./util" +{join, exists, basename, resolve} = require "path" + +module.exports = class RackApplication + # Create a `RackApplication` for the given configuration and + # root path. The application begins life in the uninitialized + # state. + constructor: (@configuration, @root, @firstHost) -> + @logger = @configuration.getLogger join "apps", basename @root + @readyCallbacks = [] + @quitCallbacks = [] + @statCallbacks = [] + + # Queue `callback` to be invoked when the application becomes ready, + # then start the initialization process. If the application's state + # is ready, the callback is invoked immediately. + ready: (callback) -> + if @state is "ready" + callback() + else + @readyCallbacks.push callback + @initialize() + + # Tell the application to quit and queue `callback` to be invoked + # when all workers have exited. If the application has already quit, + # the callback is invoked immediately. + quit: (callback) -> + if @state + @quitCallbacks.push callback if callback + @terminate() + else + callback?() + + # Stat `tmp/restart.txt` in the application root and invoke the + # given callback with a single argument indicating whether or not + # the file has been touched since the last call to + # `queryRestartFile`. + queryRestartFile: (callback) -> + fs.stat join(@root, "tmp/restart.txt"), (err, stats) => + if err + @mtime = null + callback false + else + lastMtime = @mtime + @mtime = stats.mtime.getTime() + callback lastMtime isnt @mtime + + # Check to see if `tmp/always_restart.txt` is present in the + # application root, and set the pool's `runOnce` option + # accordingly. Invoke `callback` when the existence check has + # finished. (Multiple calls to this method are aggregated.) + setPoolRunOnceFlag: (callback) -> + unless @statCallbacks.length + exists join(@root, "tmp/always_restart.txt"), (alwaysRestart) => + @pool.runOnce = alwaysRestart + statCallback() for statCallback in @statCallbacks + @statCallbacks = [] + + @statCallbacks.push callback + + # Collect environment variables from `.powrc` and `.powenv`, in that + # order, if present. The idea is that `.powrc` files can be checked + # into a source code repository for global configuration, leaving + # `.powenv` free for any necessary local overrides. + loadScriptEnvironment: (env, callback) -> + async.reduce [".powrc", ".envrc", ".powenv"], env, (env, filename, callback) => + exists script = join(@root, filename), (scriptExists) -> + if scriptExists + sourceScriptEnv script, env, callback + else + callback null, env + , callback + + # If `.rvmrc` and `$HOME/.rvm/scripts/rvm` are present, load rvm, + # source `.rvmrc`, and invoke `callback` with the resulting + # environment variables. If `.rvmrc` is present but rvm is not + # installed, invoke `callback` without sourcing `.rvmrc`. + # Before loading rvm, Pow invokes a helper script that shows a + # deprecation notice if it has not yet been displayed. + loadRvmEnvironment: (env, callback) -> + exists script = join(@root, ".rvmrc"), (rvmrcExists) => + if rvmrcExists + exists rvm = @configuration.rvmPath, (rvmExists) => + if rvmExists + libexecPath = resolve "#{__dirname}/../libexec" + before = """ + '#{libexecPath}/pow_rvm_deprecation_notice' '#{[@firstHost]}' + source '#{rvm}' > /dev/null + """.trim() + sourceScriptEnv script, env, {before}, callback + else + callback null, env + else + callback null, env + + # Stat `tmp/restart.txt` to cache its mtime, then load the + # application's full environment from `.powrc`, `.powenv`, and + # `.rvmrc`. + loadEnvironment: (callback) -> + @queryRestartFile => + @loadScriptEnvironment @configuration.env, (err, env) => + if err then callback err + else @loadRvmEnvironment env, (err, env) => + if err then callback err + else callback null, env + + # Begin the initialization process if the application is in the + # uninitialized state. (If the application is terminating, queue a + # call to `initialize` after all workers have exited.) + initialize: -> + if @state + if @state is "terminating" + @quit => @initialize() + return + + @state = "initializing" + + # Load the application's environment. If an error is raised or + # either of the environment scripts exits with a non-zero status, + # reset the application's state and log the error. + @loadEnvironment (err, env) => + if err + @state = null + @logger.error err.message + @logger.error "stdout: #{err.stdout}" + @logger.error "stderr: #{err.stderr}" + + # Set the application's state to ready. Then create the Nack + # pool instance using the `workers` and `timeout` options from + # the application's environment or the global configuration. + else + @state = "ready" + + @pool = nack.createPool join(@root, "config.ru"), + env: env + size: env?.POW_WORKERS ? @configuration.workers + idle: (env?.POW_TIMEOUT ? @configuration.timeout) * 1000 + + # Log the workers' stderr and stdout, and log each worker's + # PID as it spawns and exits. + bufferLines @pool.stdout, (line) => @logger.info line + bufferLines @pool.stderr, (line) => @logger.warning line + + @pool.on "worker:spawn", (process) => + @logger.debug "nack worker #{process.child.pid} spawned" + + @pool.on "worker:exit", (process) => + @logger.debug "nack worker exited" + + # Invoke and remove all queued callbacks, passing along the + # error, if any. + readyCallback err for readyCallback in @readyCallbacks + @readyCallbacks = [] + + # Begin the termination process. (If the application is initializing, + # wait until it is ready before shutting down.) + terminate: -> + if @state is "initializing" + @ready => @terminate() + + else if @state is "ready" + @state = "terminating" + + # Instruct all workers to exit. After the processes have + # terminated, reset the application's state, then invoke and + # remove all queued callbacks. + @pool.quit => + @state = null + @mtime = null + @pool = null + + quitCallback() for quitCallback in @quitCallbacks + @quitCallbacks = [] + + # Handle an incoming HTTP request. Wait until the application is in + # the ready state, restart the workers if necessary, then pass the + # request along to the Nack pool. If the Nack worker raises an + # exception handling the request, reset the application. + handle: (req, res, next, callback) -> + resume = pause req + @ready (err) => + return next err if err + @setPoolRunOnceFlag => + @restartIfNecessary => + req.proxyMetaVariables = + SERVER_PORT: @configuration.dstPort.toString() + try + @pool.proxy req, res, (err) => + @quit() if err + next err + finally + resume() + callback?() + + # Terminate the application, re-initialize it, and invoke the given + # callback when the application's state becomes ready. + restart: (callback) -> + @quit => + @ready callback + + # Restart the application if `tmp/restart.txt` has been touched + # since the last call to this function. + restartIfNecessary: (callback) -> + @queryRestartFile (mtimeChanged) => + if mtimeChanged + @restart callback + else + callback() + + # Append RVM autoload boilerplate to the application's `.powrc` + # file. This is called by the RVM deprecation notice mini-app. + writeRvmBoilerplate: -> + powrc = join @root, ".powrc" + boilerplate = @constructor.rvmBoilerplate + + fs.readFile powrc, "utf8", (err, contents) -> + contents ?= "" + if contents.indexOf(boilerplate) is -1 + fs.writeFile powrc, "#{boilerplate}\n#{contents}" + + @rvmBoilerplate: """ + if [ -f "$rvm_path/scripts/rvm" ] && [ -f ".rvmrc" ]; then + source "$rvm_path/scripts/rvm" + source ".rvmrc" + fi + """ |
