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 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
|
# Phusion Passenger - http://www.modrails.com/
# Copyright (c) 2010 Phusion
#
# "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'rubygems'
require 'phusion_passenger/abstract_server'
require 'phusion_passenger/abstract_server_collection'
require 'phusion_passenger/app_process'
require 'phusion_passenger/classic_rails/application_spawner'
require 'phusion_passenger/exceptions'
require 'phusion_passenger/constants'
require 'phusion_passenger/utils'
module PhusionPassenger
module ClassicRails
# This class is capable of spawning Ruby on Rails application instances
# quickly. This is done by preloading the Ruby on Rails framework into memory,
# before spawning the application instances.
#
# A single FrameworkSpawner instance can only hold a single Ruby on Rails
# framework version. So be careful when using FrameworkSpawner: the applications
# that you spawn through it must require the same RoR version. To handle multiple
# RoR versions, use multiple FrameworkSpawner instances.
#
# FrameworkSpawner uses ApplicationSpawner internally.
#
# *Note*: FrameworkSpawner may only be started asynchronously with AbstractServer#start.
# Starting it synchronously with AbstractServer#start_synchronously has not been tested.
class FrameworkSpawner < AbstractServer
include Utils
# This exception means that the FrameworkSpawner server process exited unexpectedly.
class Error < AbstractServer::ServerError
end
# Creates a new instance of FrameworkSpawner.
#
# Extra supported options:
# - <tt>framework_version</tt>: The Ruby on Rails version to use. It is not checked whether
# this version is actually installed.
#
# All other options will be passed on to ApplicationSpawner and RequestHandler.
#
# Note that the specified Rails framework will be loaded during the entire life time
# of the FrameworkSpawner server. If you wish to reload the Rails framework's code,
# then restart the server by calling AbstractServer#stop and AbstractServer#start.
def initialize(options = {})
if !options.respond_to?(:'[]')
raise ArgumentError, "The 'options' argument does not seem to be an options hash"
end
@framework_version = options["framework_version"]
if options.has_key?("print_framework_loading_exceptions")
@print_framework_loading_exceptions = options["print_framework_loading_exceptions"]
else
@print_framework_loading_exceptions = true
end
if !@framework_version
raise ArgumentError, "The 'framework_version' option must specified"
end
super()
@options = options
self.max_idle_time = DEFAULT_FRAMEWORK_SPAWNER_MAX_IDLE_TIME
define_message_handler(:spawn_application, :handle_spawn_application)
define_message_handler(:reload, :handle_reload)
end
# Overrided from AbstractServer#start.
#
# May raise these additional exceptions:
# - FrameworkInitError: An error occurred while loading the specified Ruby on Rails framework.
# - FrameworkSpawner::Error: The FrameworkSpawner server exited unexpectedly.
def start
super
begin
channel = MessageChannel.new(@owner_socket)
result = channel.read
if result.nil?
raise Error, "The framework spawner server exited unexpectedly."
else
status = result[0]
end
if status == 'exception'
child_exception = unmarshal_exception(channel.read_scalar)
stop
message = "Could not load Ruby on Rails framework version #{@framework_version}: " <<
"#{child_exception.class} (#{child_exception.message})"
options = { :version => @framework_version }
if @print_framework_loading_exceptions
print_exception(self.class.to_s, child_exception)
end
raise FrameworkInitError.new(message, child_exception, options)
end
rescue IOError, SystemCallError, SocketError => e
stop if started?
raise Error, "The framework spawner server exited unexpectedly: #{e}"
rescue
stop if started?
raise
end
end
# Spawn a RoR application using the Ruby on Rails framework
# version associated with this FrameworkSpawner.
# When successful, an Application object will be returned, which represents
# the spawned RoR application.
#
# All options accepted by ApplicationSpawner.new and RequestHandler.new are accepted.
#
# FrameworkSpawner 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 FrameworkSpawner by calling AbstractServer#stop, then AbstractServer#start.
# - Reload the application by calling reload with the correct app_root argument.
#
# Raises:
# - AbstractServer::ServerNotStarted: The FrameworkSpawner server hasn't already been started.
# - AppInitError: The application raised an exception or called exit() during startup.
# - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly.
# - FrameworkSpawner::Error: The FrameworkSpawner server exited unexpectedly.
def spawn_application(options = {})
app_root = options["app_root"]
options = sanitize_spawn_options(options)
options["app_root"] = app_root
# No need for the ApplicationSpawner to print exceptions. All
# exceptions raised by the ApplicationSpawner are sent back here,
# so we just need to decide here whether we want to print it.
print_exceptions = options["print_exceptions"]
options["print_exceptions"] = false
begin
connect do |channel|
channel.write("spawn_application", *options.to_a.flatten)
result = channel.read
if result.nil?
raise IOError, "Connection closed"
end
if result[0] == 'exception'
e = unmarshal_exception(channel.read_scalar)
if print_exceptions && e.respond_to?(:child_exception) && e.child_exception
print_exception(self.class.to_s, e.child_exception)
elsif print_exceptions
print_exception(self.class.to_s, e)
end
raise e
else
return AppProcess.read_from_channel(channel)
end
end
rescue SystemCallError, IOError, SocketError => e
raise Error, "The framework spawner server exited unexpectedly: #{e}"
end
end
# Remove the cached application instances at the given group name.
# If nil is specified as group name, then all cached application
# instances will be removed, no matter the group name.
#
# <b>Long description:</b>
# Application code might be cached in memory by a FrameworkSpawner. 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:
# - FrameworkSpawner::Error: The FrameworkSpawner server exited unexpectedly.
def reload(app_group_name = nil)
connect do |channel|
if app_group_name.nil?
channel.write("reload")
else
channel.write("reload", app_group_name)
end
end
rescue SystemCallError, IOError, SocketError
raise Error, "The framework spawner server exited unexpectedly: #{e}"
end
protected
# Overrided method.
def before_fork # :nodoc:
if GC.copy_on_write_friendly?
# Garbage collect now so that the child process doesn't have to
# do that (to prevent making pages dirty).
GC.start
end
end
# Overrided method.
def initialize_server # :nodoc:
$0 = "Passenger FrameworkSpawner: #{@framework_version}"
@spawners = AbstractServerCollection.new
channel = MessageChannel.new(@owner_socket)
begin
preload_rails
rescue StandardError, ScriptError, NoMemoryError => e
channel.write('exception')
channel.write_scalar(marshal_exception(e))
return
end
channel.write('success')
end
# Overrided method.
def finalize_server # :nodoc:
@spawners.cleanup
end
private
def preload_rails
Object.const_set(:RAILS_ROOT, ".")
gem 'rails', "=#{@framework_version}"
require 'initializer'
require 'active_support'
require 'active_record'
require 'action_controller'
require 'action_view'
require 'action_pack'
require 'action_mailer'
require 'dispatcher'
begin
if ::Rails::VERSION::MAJOR >= 2
require 'active_resource'
else
require 'action_web_service'
end
require 'ruby_version_check'
require 'active_support/whiny_nil'
rescue NameError
# Rails < 1.1
require 'action_web_service'
end
Object.send(:remove_const, :RAILS_ROOT)
end
def handle_spawn_application(client, *options)
app_process = nil
options = sanitize_spawn_options(Hash[*options])
app_group_name = options["app_group_name"]
@spawners.synchronize do
begin
spawner = @spawners.lookup_or_add(app_group_name) do
spawner = ApplicationSpawner.new(@options.merge(options))
if options["app_spawner_timeout"] && options["app_spawner_timeout"] != -1
spawner.max_idle_time = options["app_spawner_timeout"]
end
spawner.start
spawner
end
rescue InvalidPath, AppInitError, ApplicationSpawner::Error => e
client.write('exception')
client.write_scalar(marshal_exception(e))
if e.respond_to?(:child_exception) && e.child_exception.is_a?(LoadError)
# 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
end
return
end
begin
app_process = spawner.spawn_application(options)
rescue ApplicationSpawner::Error => e
spawner.stop
@spawners.delete(app_group_name)
client.write('exception')
client.write_scalar(marshal_exception(e))
return
end
end
client.write('success')
app_process.write_to_channel(client)
ensure
app_process.close if app_process
end
def handle_reload(client, app_group_name = nil)
@spawners.synchronize do
if app_group_name
@spawners.delete(app_group_name)
else
@spawners.clear
end
end
end
end
end # module ClassicRails
end # module PhusionPassenger
|