Class: Volt::ForkingServer
Instance Method Summary collapse
-
#boot_error(error) ⇒ Object
called from the child when the boot failes.
- #call(env) ⇒ Object
-
#call_on_child(env_base, env_other) ⇒ Object
When passing an object, Drb will not marshal it if any of its subobjects are not marshalable.
-
#initialize(server) ⇒ ForkingServer
constructor
A new instance of ForkingServer.
- #reload(changed_files) ⇒ Object
- #start_change_listener ⇒ Object
-
#start_child ⇒ Object
Start child forks off a child process and sets up a DRb connection to the child.
- #stop_change_listener ⇒ Object
- #stop_child ⇒ Object
- #sync_mod_time ⇒ Object
- #update_mod_time ⇒ Object
-
#watch_for_parent_exit ⇒ Object
In the even the parent gets killed without at_exit running, we watch the pipe and close if the pipe gets closed.
Constructor Details
#initialize(server) ⇒ ForkingServer
Returns a new instance of ForkingServer.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/volt/server/forking_server.rb', line 18 def initialize(server) # A read write lock for accessing and creating the lock @child_lock = ReadWriteLock.new # Trap exit at_exit do # Only run on parent if @child_id puts 'Exiting...' @exiting = true stop_child end end @server = server # Set the mod time on boot update_mod_time start_child end |
Instance Method Details
#boot_error(error) ⇒ Object
called from the child when the boot failes. Sets up an error page rack app to show the user the error and handle reloading requests.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/volt/server/forking_server.rb', line 102 def boot_error(error) msg = error.inspect if error.respond_to?(:backtrace) msg << "\n" + error.backtrace.join("\n") end Volt.logger.error(msg) # Only require when needed require 'cgi' @rack_app = Proc.new do path = File.join(File.dirname(__FILE__), "forking_server/boot_error.html.erb") html = File.read(path) error_page = ERB.new(html, nil, '-').result(binding) [500, {"Content-Type" => "text/html"}, error_page] end @dispatcher = ErrorDispatcher.new end |
#call(env) ⇒ Object
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/volt/server/forking_server.rb', line 188 def call(env) @child_lock.with_read_lock do if @exiting [500, {}, 'Server Exiting'] else env_base = {} env_other = {} env.each_pair do |key, value| if [String, TrueClass, FalseClass, Array].include?(value.class) env_base.merge!(key => value) else env_other.merge!(key => value) end end status, headers, body_str = @server_proxy.call_on_child(env_base, env_other) [status, headers, StringIO.new(body_str)] end end end |
#call_on_child(env_base, env_other) ⇒ Object
When passing an object, Drb will not marshal it if any of its subobjects are not marshalable. So we split the marshable and not marshalbe objects then re-merge them so we get real copies of most values (which are needed in some cases) Then we merge them back into a new hash.
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 |
# File 'lib/volt/server/forking_server.rb', line 157 def call_on_child(env_base, env_other) env = env_base # TODO: this requires quite a few trips, there's probably a faster way # to handle this. env_other.each_pair do |key, value| env[key] = value end status, headers, body = @rack_app.call(env) # Extract the body to pass as a string. We need to do this # because after the call, the objects will be GC'ed, so we want # them to be able to be marshaled to be send over DRb. if body.respond_to?(:to_str) body_str = body else extracted_body = [] # Read the body.each do |str| extracted_body << str end body.close if body.respond_to?(:close) body_str = extracted_body.join end [status, headers, body_str] end |
#reload(changed_files) ⇒ Object
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 |
# File 'lib/volt/server/forking_server.rb', line 212 def reload(changed_files) # only reload the server code if a non-view file was changed server_code_changed = changed_files.any? { |path| File.extname(path) == '.rb' } msg = 'file changed, reloading' msg << ' server and' if server_code_changed msg << ' client...' Volt.logger.log_with_color(msg, :light_blue) # Figure out if any views or routes were changed: # TODO: Might want to only check for /config/ under the CWD if changed_files.any? {|path| path =~ /\/config\// } update_mod_time sync_mod_time end begin SocketConnectionHandler.(nil, 'reload') rescue => e Volt.logger.error('Reload dispatch error: ') Volt.logger.error(e) end if server_code_changed @child_lock.with_write_lock do stop_child start_child sync_mod_time end end end |
#start_change_listener ⇒ Object
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/volt/server/forking_server.rb', line 258 def start_change_listener sync_mod_time = {} if ENV['POLL_FS'] [:force_polling] = true end # Setup the listeners for file changes @listener = Listen.to("#{@server.app_path}/", ) do |modified, added, removed| Thread.new do # Run the reload in a new thread reload(modified + added + removed) end end @listener.start end |
#start_child ⇒ Object
Start child forks off a child process and sets up a DRb connection to the child. #start_child should be called from within the write lock.
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 |
# File 'lib/volt/server/forking_server.rb', line 42 def start_child # Aquire the write lock, so we prevent anyone from using the child until # its setup or recreated. unless @drb_object # Get the id of the parent process, so we can wait for exit in the child # so the child can exit if the parent closes. @parent_id = Process.pid @reader, @writer = IO.pipe if @child_id = fork # running as parent @writer.close # Read the url from the child uri = @reader.gets.strip # Setup a drb object to the child DRb.start_service @drb_object = DRbObject.new_with_uri(uri) @server_proxy = @drb_object[0] @dispatcher_proxy = @drb_object[1] SocketConnectionHandler.dispatcher = @dispatcher_proxy start_change_listener else # Running as child @reader.close watch_for_parent_exit begin volt_app = @server.boot_volt @rack_app = volt_app.middleware # Set the drb object locally @dispatcher = Dispatcher.new(volt_app) rescue Exception => error boot_error(error) end drb_object = DRb.start_service('drbunix:', [self, @dispatcher]) @writer.puts(drb_object.uri) begin DRb.thread.join rescue Interrupt => e # Ignore interrupt exit end end end end |
#stop_change_listener ⇒ Object
276 277 278 |
# File 'lib/volt/server/forking_server.rb', line 276 def stop_change_listener @listener.stop end |
#stop_child ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/volt/server/forking_server.rb', line 123 def stop_child # clear the drb object and kill the child process. if @drb_object begin @drb_object = nil DRb.stop_service @reader.close stop_change_listener Process.kill(9, @child_id) rescue => e puts "Stop Child Error: #{e.inspect}" end end end |
#sync_mod_time ⇒ Object
250 251 252 253 254 255 256 |
# File 'lib/volt/server/forking_server.rb', line 250 def sync_mod_time disp = SocketConnectionHandler.dispatcher unless disp.is_a?(ErrorDispatcher) disp.component_modified(@last_mod_time) end end |
#update_mod_time ⇒ Object
246 247 248 |
# File 'lib/volt/server/forking_server.rb', line 246 def update_mod_time @last_mod_time = Time.now.to_i.to_s end |
#watch_for_parent_exit ⇒ Object
In the even the parent gets killed without at_exit running, we watch the pipe and close if the pipe gets closed.
140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/volt/server/forking_server.rb', line 140 def watch_for_parent_exit Thread.new do loop do if @writer.closed? puts 'Parent process died' exit end sleep 3 end end end |