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
# 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

  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.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/volt/server/forking_server.rb', line 99

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



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/volt/server/forking_server.rb', line 185

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.



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
# File 'lib/volt/server/forking_server.rb', line 154

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



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/volt/server/forking_server.rb', line 209

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)

  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
    end
  end
end

#start_change_listenerObject



234
235
236
237
238
239
240
241
242
243
# File 'lib/volt/server/forking_server.rb', line 234

def start_change_listener
  # 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_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.



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
# File 'lib/volt/server/forking_server.rb', line 39

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

      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)

      watch_for_parent_exit

      begin
        DRb.thread.join
      rescue Interrupt => e
        # Ignore interrupt
        exit
      end
    end
  end
end

#stop_change_listenerObject



245
246
247
# File 'lib/volt/server/forking_server.rb', line 245

def stop_change_listener
  @listener.stop
end

#stop_childObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/volt/server/forking_server.rb', line 120

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

#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.



137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/volt/server/forking_server.rb', line 137

def watch_for_parent_exit
  Thread.new do
    loop do
      if @writer.closed?
        puts 'Parent process died'
        exit
      end

      sleep 3
    end
  end
end