rails spring的实现原理二 Server部分

上一章我们主要了解了spring的client部分的代码,那么这一张我们从server_boot开始了解spring server部分的代码。
boot_server主要是运行了如下命令:

def server_command
  ENV["SPRING_SERVER_COMMAND"] || "#{File.expand_path("../../../bin/spring", __FILE__)} server --background"
end

也就是Client::Server文件

module Spring
  module Client
    class Server < Command
      def self.description
        "Explicitly start a Spring server in the foreground"
      end

      def call
        require "spring/server"
        Spring::Server.boot(foreground: foreground?)
      end

      def foreground?
        !args.include?("--background")
      end
    end
  end
end

然后看到这句代码Spring::Server.boot(foreground: foreground?):

# spring/server
def self.boot(options = {})
  new(options).boot
end

def boot
  Spring.verify_environment

  write_pidfile
  set_pgid unless foreground?
  ignore_signals unless foreground?
  set_exit_hook
  set_process_title
  start_server
end

boot设置好pid hook title等之后调用了start_server

def start_server
  server = UNIXServer.open(env.socket_name)
  log "started on #{env.socket_name}"
  loop { serve server.accept }
rescue Interrupt
end

1.打开了UNIXServer就是上一张我们client需要连接的那个socket:
2.等在server.accept到client发过来的信息:

def serve(client)
  log "accepted client"
  client.puts env.version

  app_client = client.recv_io
  command    = JSON.load(client.read(client.gets.to_i))

  args, default_rails_env = command.values_at('args', 'default_rails_env')

  if Spring.command?(args.first)
    log "running command #{args.first}"
    client.puts
    client.puts @applications[rails_env_for(args, default_rails_env)].run(app_client)
  else
    log "command not found #{args.first}"
    client.close
  end
rescue SocketError => e
  raise e unless client.eof?
ensure
  redirect_output
end

serve里主要的工作:
1.发送version给client
2.收到client那边send_io过来的对象
3.拿到client发送过来的command
4.@applications[rails_env_for(args, default_rails_env)].run(app_client),根据环境执行相应的command.

看到ApplicationManager里的run方法:
最后开启了一个新的线程,主要执行了 application/boot里的代码,并且将child_socket设置成文件描述符3,日志的socket设置成文件描述符4.

def run(client)
  with_child do
    child.send_io client
    child.gets or raise Errno::EPIPE
  end

  pid = child.gets.to_i

  unless pid.zero?
    log "got worker pid #{pid}"
    pid
  end
rescue Errno::ECONNRESET, Errno::EPIPE => e
  log "#{e} while reading from child; returning no pid"
  nil
ensure
  client.close
end

def with_child
  synchronize do
    if alive?
      begin
        yield
      rescue Errno::ECONNRESET, Errno::EPIPE
        # The child has died but has not been collected by the wait thread yet,
        # so start a new child and try again.
        log "child dead; starting"
        start
        yield
      end
    else
      log "child not running; starting"
      start
      yield
    end
  end
end

def start
  start_child
end

def start_child(preload = false)
  @child, child_socket = UNIXSocket.pair

  Bundler.with_clean_env do
    @pid = Process.spawn(
      {
        "RAILS_ENV"           => app_env,
        "RACK_ENV"            => app_env,
        "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
        "SPRING_PRELOAD"      => preload ? "1" : "0"
      },
      "ruby",
      "-I", File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first),
      "-I", File.expand_path("../..", __FILE__),
      "-e", "require 'spring/application/boot'",
      3 => child_socket,
      4 => spring_env.log_file,
    )
  end

  start_wait_thread(pid, child) if child.gets
  child_socket.close
end

def start_wait_thread(pid, child)
  Process.detach(pid)

  Spring.failsafe_thread {
    # The recv can raise an ECONNRESET, killing the thread, but that's ok
    # as if it does we're no longer interested in the child
    loop do
      IO.select([child])
      break if child.recv(1, Socket::MSG_PEEK).empty?
      sleep 0.01
    end

    log "child #{pid} shutdown"

    synchronize {
      if @pid == pid
        @pid = nil
        restart
      end
    }
  }
end

以上代码最后主要是为了开启一个进程,那么我们看下 spring/application/boot里的代码:

# This is necessary for the terminal to work correctly when we reopen stdin.
Process.setsid

require "spring/application"

app = Spring::Application.new(
  UNIXSocket.for_fd(3),
  Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup),
  Spring::Env.new(log_file: IO.for_fd(4))
)

Signal.trap("TERM") { app.terminate }

Spring::ProcessTitleUpdater.run { |distance|
  "spring app    | #{app.app_name} | started #{distance} ago | #{app.app_env} mode"
}

app.eager_preload if ENV.delete("SPRING_PRELOAD") == "1"
app.run

实例化一个application类,然后调用run方法:

def run
  state :running
  manager.puts

  loop do
    IO.select [manager, @interrupt.first]

    if terminating? || watcher_stale? || preload_failed?
      exit
    else
      serve manager.recv_io(UNIXSocket)
    end
  end
end

def serve(client)
  log "got client"
  manager.puts

  stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
  [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }

  preload unless preloaded?

  args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
  command   = Spring.command(args.shift)

  connect_database
  setup command

  if Rails.application.reloaders.any?(&:updated?)
    # Rails 5.1 forward-compat. AD::R is deprecated to AS::R in Rails 5.
    if defined? ActiveSupport::Reloader
      Rails.application.reloader.reload!
    else
      ActionDispatch::Reloader.cleanup!
      ActionDispatch::Reloader.prepare!
    end
  end

  pid = fork {
    Process.setsid
    IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
    trap("TERM", "DEFAULT")

    STDERR.puts "Running via Spring preloader in process #{Process.pid}" unless Spring.quiet

    ARGV.replace(args)
    $0 = command.exec_name

    # Delete all env vars which are unchanged from before spring started
    original_env.each { |k, v| ENV.delete k if ENV[k] == v }

    # Load in the current env vars, except those which *were* changed when spring started
    env.each { |k, v| ENV[k] ||= v }

    # requiring is faster, so if config.cache_classes was true in
    # the environment's config file, then we can respect that from
    # here on as we no longer need constant reloading.
    if @original_cache_classes
      ActiveSupport::Dependencies.mechanism = :require
      Rails.application.config.cache_classes = true
    end

    connect_database
    srand

    invoke_after_fork_callbacks
    shush_backtraces

    command.call
  }

  disconnect_database

  log "forked #{pid}"
  manager.puts pid

  wait pid, streams, client
rescue Exception => e
  log "exception: #{e}"
  manager.puts unless pid

  if streams && !e.is_a?(SystemExit)
    print_exception(stderr, e)
    streams.each(&:close)
  end

  client.puts(1) if pid
  client.close
ensure
  # Redirect STDOUT and STDERR to prevent from keeping the original FDs
  # (i.e. to prevent `spring rake -T | grep db` from hanging forever),
  # even when exception is raised before forking (i.e. preloading).
  reset_streams
end

def wait(pid, streams, client)
  @mutex.synchronize { @waiting << pid }

  # Wait in a separate thread so we can run multiple commands at once
  Spring.failsafe_thread {
    begin
      _, status = Process.wait2 pid
      log "#{pid} exited with #{status.exitstatus}"

      streams.each(&:close)
      client.puts(status.exitstatus)
      client.close
    ensure
      @mutex.synchronize { @waiting.delete pid }
      exit_if_finished
    end
  }

  Spring.failsafe_thread {
    while signal = client.gets.chomp
      begin
        Process.kill(signal, -Process.getpgid(pid))
        client.puts(0)
      rescue Errno::ESRCH
        client.puts(1)
      end
    end
  }
end

def reset_streams
  [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
  STDIN.reopen("/dev/null")
end

command.call调用了rails console的命令,将console加入到ARGV,然后load rails 目录下的bin/rails文件,这时就会执行rails c 命令,显然这有点绕看了好几次才明白。

def call
  ARGV.unshift command_name
  load Dir.glob(::Rails.root.join("{bin,script}/rails")).first
end

附上流程图:


image.png

你可能感兴趣的:(rails spring的实现原理二 Server部分)