class Volt::ForkingServer

Public Class Methods

new(server) click to toggle source
# 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

Public Instance Methods

boot_error(error) click to toggle source

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.

# 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) click to toggle source
# 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) click to toggle source

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.

# 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) click to toggle source
# 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_listener() click to toggle source
# 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_child() click to toggle source

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.

# 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() click to toggle source
# File lib/volt/server/forking_server.rb, line 276
def stop_change_listener
  @listener.stop
end
stop_child() click to toggle source
# 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() click to toggle source
# 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() click to toggle source
# 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() click to toggle source

In the even the parent gets killed without at_exit running, we watch the pipe and close if the pipe gets closed.

# 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