public
Description: Phusion Passenger (mod_rails)
Homepage: http://www.modrails.com/
Clone URL: git://github.com/FooBarWidget/passenger.git
Click here to lend your support to: passenger and make a donation at www.pledgie.com !
Hongli Lai (Phusion) (author)
Thu May 01 12:13:06 -0700 2008
commit  53301de464b323d364723854d3a8d293ab8327d6
tree    23e53e1e40d0fea9ecd6312dca292b3e7f2f5cb8
parent  3f5fb12ba8240018c6210a42d2550d21a3cd6497
passenger / lib / passenger / spawn_manager.rb
100644 258 lines (244 sloc) 8.809 kb
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
257
258
# Phusion Passenger - http://www.modrails.com/
# Copyright (C) 2008 Phusion
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
require 'passenger/abstract_server'
require 'passenger/framework_spawner'
require 'passenger/application'
require 'passenger/message_channel'
require 'passenger/html_template'
require 'passenger/utils'
require 'passenger/platform_info'
module Passenger
 
# This class is capable of spawning Ruby on Rails application instances.
# Spawning a Ruby on Rails application is usually slow. But SpawnManager
# will preload and cache Ruby on Rails frameworks, as well as application
# code, so subsequent spawns will be very fast.
#
# Internally, SpawnManager uses FrameworkSpawner to preload and cache
# Ruby on Rails frameworks. FrameworkSpawner, in turn, uses
# ApplicationSpawner to preload and cache application code.
#
# *Note*: SpawnManager may only be started synchronously with
# AbstractServer#start_synchronously. Starting asynchronously has not been
# tested. Don't forget to call cleanup after the server's main loop has
# finished.
class SpawnManager < AbstractServer
  DEFAULT_INPUT_FD = 3
  FRAMEWORK_SPAWNER_MAX_IDLE_TIME = 30 * 60
  APP_SPAWNER_MAX_IDLE_TIME = FrameworkSpawner::APP_SPAWNER_MAX_IDLE_TIME
  SPAWNER_CLEAN_INTERVAL = [FRAMEWORK_SPAWNER_MAX_IDLE_TIME,
    APP_SPAWNER_MAX_IDLE_TIME].min + 5
  
  include Utils
  
  def initialize
    super()
    @spawners = {}
    @lock = Mutex.new
    @cond = ConditionVariable.new
    @cleaner_thread = Thread.new do
      cleaner_thread_main
    end
    define_message_handler(:spawn_application, :handle_spawn_application)
    define_message_handler(:reload, :handle_reload)
    define_signal_handler('SIGHUP', :reload)
  end
 
  # Spawn a RoR application When successful, an Application object will be
  # returned, which represents the spawned RoR application.
  #
  # See ApplicationSpawner.new for an explanation of the +lower_privilege+
  # and +lowest_user+ parameters.
  #
  # SpawnManager will internally cache the code of applications, in order to
  # speed up future spawning attempts. This implies that, if you've
  # changed the application's code, you must do one of these things:
  # - Restart this SpawnManager by calling AbstractServer#stop, then AbstractServer#start.
  # - Reload the application by calling reload with the correct app_root argument.
  #
  # Raises:
  # - ArgumentError: +app_root+ doesn't appear to be a valid Ruby on Rails application root.
  # - VersionNotFound: The Ruby on Rails framework version that the given application requires
  # is not installed.
  # - AbstractServer::ServerError: One of the server processes exited unexpectedly.
  # - FrameworkInitError: The Ruby on Rails framework that the application requires could not be loaded.
  # - AppInitError: The application raised an exception or called exit() during startup.
  def spawn_application(app_root, lower_privilege = true, lowest_user = "nobody")
    framework_version = Application.detect_framework_version(app_root)
    if framework_version == :vendor
      vendor_path = normalize_path("#{app_root}/vendor/rails")
      key = "vendor:#{vendor_path}"
      create_spawner = proc do
        FrameworkSpawner.new(:vendor => vendor_path)
      end
    elsif framework_version.nil?
      app_root = normalize_path(app_root)
      key = "app:#{app_root}"
      create_spawner = proc do
        ApplicationSpawner.new(app_root, lower_privilege, lowest_user)
      end
    else
      key = "version:#{framework_version}"
      create_spawner = proc do
        FrameworkSpawner.new(:version => framework_version)
      end
    end
    
    spawner = nil
    @lock.synchronize do
      spawner = @spawners[key]
      if !spawner
        spawner = create_spawner.call
        spawner.start
        @spawners[key] = spawner
      end
      spawner.time = Time.now
      begin
        if spawner.is_a?(FrameworkSpawner)
          return spawner.spawn_application(app_root, lower_privilege,
            lowest_user)
        else
          return spawner.spawn_application
        end
      rescue AbstractServer::ServerError
        spawner.stop
        @spawners.delete(key)
        raise
      end
    end
  end
  
  # Remove the cached application instances at the given application root.
  # If nil is specified as application root, then all cached application
  # instances will be removed, no matter the application root.
  #
  # <b>Long description:</b>
  # Application code might be cached in memory. But once it a while, it will
  # be necessary to reload the code for an application, such as after
  # deploying a new version of the application. This method makes sure that
  # any cached application code is removed, so that the next time an
  # application instance is spawned, the application code will be freshly
  # loaded into memory.
  #
  # Raises AbstractServer::SpawnError if something went wrong.
  def reload(app_root = nil)
    if app_root
      begin
        app_root = normalize_path(app_root)
      rescue ArgumentError
      end
    end
    @lock.synchronize do
      if app_root
        # Delete associated ApplicationSpawner.
        key = "app:#{app_root}"
        spawner = @spawners[key]
        if spawner
          spawner.stop
          @spawners.delete(key)
        end
      end
      @spawners.each_value do |spawner|
        # Reload FrameworkSpawners.
        if spawner.respond_to?(:reload)
          spawner.reload(app_root)
        end
      end
    end
  end
  
  # Cleanup resources. Should be called when this SpawnManager is no longer needed.
  def cleanup
    @lock.synchronize do
      @cond.signal
    end
    @cleaner_thread.join
    @lock.synchronize do
      @spawners.each_value do |spawner|
        spawner.stop
      end
      @spawners.clear
    end
  end
 
private
  def handle_spawn_application(app_root, lower_privilege, lowest_user)
    lower_privilege = lower_privilege == "true"
    app = nil
    begin
      app = spawn_application(app_root, lower_privilege, lowest_user)
    rescue ArgumentError => e
      send_error_page(client, 'invalid_app_root', :error => e, :app_root => app_root)
    rescue AbstractServer::ServerError => e
      send_error_page(client, 'general_error', :error => e)
    rescue VersionNotFound => e
      send_error_page(client, 'version_not_found', :error => e, :app_root => app_root)
    rescue AppInitError => e
      if (defined?(Mysql::Error) && e.child_exception.is_a?(Mysql::Error)) ||
       (e.child_exception.is_a?(UnknownError) && e.child_exception.real_class_name =~ /^ActiveRecord/)
        send_error_page(client, 'database_error', :error => e,
          :app_root => app_root)
      elsif e.child_exception.is_a?(LoadError) ||
       (e.child_exception.is_a?(UnknownError) && e.child_exception.real_class_name == "MissingSourceFile")
        # A source file failed to load, maybe because of a
        # missing gem. If that's the case then the sysadmin
        # will install probably the gem. So we clear RubyGems's
        # cache so that it can detect new gems.
        Gem.clear_paths
        send_error_page(client, 'load_error', :error => e, :app_root => app_root)
      elsif e.child_exception.nil?
        send_error_page(client, 'app_exited_during_initialization', :error => e,
          :app_root => app_root)
      else
        send_error_page(client, 'app_init_error', :error => e,
          :app_root => app_root)
      end
    rescue FrameworkInitError => e
      send_error_page(client, 'framework_init_error', :error => e)
    end
    if app
      client.write('ok')
      client.write(app.pid, app.listen_socket_name, app.using_abstract_namespace?)
      client.send_io(app.owner_pipe)
      app.close
    end
  end
  
  def handle_reload(app_root)
    reload(app_root)
  end
  
  def cleaner_thread_main
    @lock.synchronize do
      while true
        if @cond.timed_wait(@lock, SPAWNER_CLEAN_INTERVAL)
          break
        else
          current_time = Time.now
          @spawners.keys.each do |key|
            spawner = @spawners[key]
            if spawner.is_a?(FrameworkSpawner)
              max_idle_time = FRAMEWORK_SPAWNER_MAX_IDLE_TIME
            else
              max_idle_time = APP_SPAWNER_MAX_IDLE_TIME
            end
            if current_time - spawner.time > max_idle_time
              spawner.stop
              @spawners.delete(key)
            end
          end
        end
      end
    end
  end
  
  def send_error_page(channel, template_name, options = {})
    options["enterprisey"] = File.exist?("#{File.dirname(__FILE__)}/../../enterprisey.txt")
    data = HTMLTemplate.new(template_name, options).result
    channel.write('error_page')
    channel.write_scalar(data)
  end
end
 
end # module Passenger