Class: Volt::ForkingServer

Inherits:
Object show all
Defined in:
lib/volt/server/forking_server.rb

Instance Method Summary collapse

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.send_message_all(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_listenerObject



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

  options = {}
  if ENV['POLL_FS']
    options[:force_polling] = true
  end

  # Setup the listeners for file changes
  @listener = Listen.to("#{@server.app_path}/", options) do |modified, added, removed|
    Thread.new do
      # Run the reload in a new thread
      reload(modified + added + removed)
    end
  end
  @listener.start
end

#start_childObject

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_listenerObject



276
277
278
# File 'lib/volt/server/forking_server.rb', line 276

def stop_change_listener
  @listener.stop
end

#stop_childObject



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_timeObject



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_timeObject



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_exitObject

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