aboutsummaryrefslogtreecommitdiff
path: root/samples/CoffeeScript/rack_application.coffee
blob: 674313b594262a6e599ae6b9b87edef87ab191a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
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
  """