aboutsummaryrefslogtreecommitdiff
path: root/samples/CoffeeScript/rack_application.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'samples/CoffeeScript/rack_application.coffee')
-rw-r--r--samples/CoffeeScript/rack_application.coffee256
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
+ """