class Expedite::Application

Attributes

env[R]
manager[R]
variant[R]

Public Class Methods

new(variant:, manager:, env:) click to toggle source
# File lib/expedite/application.rb, line 30
def initialize(variant:, manager:, env:)
  @variant      = variant
  @manager      = manager
  @env          = env
  @mutex        = Mutex.new
  @waiting      = Set.new
  @preloaded    = false
  @state        = :initialized
  @interrupt    = IO.pipe
end

Public Instance Methods

app_name() click to toggle source
# File lib/expedite/application.rb, line 65
def app_name
  env.app_name
end
boot() click to toggle source
# File lib/expedite/application.rb, line 41
def boot
  # This is necessary for the terminal to work correctly when we reopen stdin.
  Process.setsid rescue Errno::EPERM

  Expedite.app = self

  Signal.trap("TERM") { terminate }

  env.load_helper
  eager_preload if false #if ENV.delete("SPRING_PRELOAD") == "1"
  run
end
connect_database() click to toggle source
# File lib/expedite/application.rb, line 231
def connect_database
  ActiveRecord::Base.establish_connection if active_record_configured?
end
disconnect_database() click to toggle source
# File lib/expedite/application.rb, line 227
def disconnect_database
  ActiveRecord::Base.remove_connection if active_record_configured?
end
eager_preload() click to toggle source
# File lib/expedite/application.rb, line 102
def eager_preload
  with_pty { preload }
end
exit() click to toggle source
# File lib/expedite/application.rb, line 210
def exit
  state :exiting
  manager.shutdown(:RDWR)
  exit_if_finished
  sleep
end
exit_if_finished() click to toggle source
# File lib/expedite/application.rb, line 217
def exit_if_finished
  @mutex.synchronize {
    Kernel.exit if exiting? && @waiting.empty?
  }
end
exiting?() click to toggle source
# File lib/expedite/application.rb, line 81
def exiting?
  @state == :exiting
end
initialized?() click to toggle source
# File lib/expedite/application.rb, line 89
def initialized?
  @state == :initialized
end
invoke_after_fork_callbacks() click to toggle source
# File lib/expedite/application.rb, line 223
def invoke_after_fork_callbacks
  # TODO:
end
log(message) click to toggle source
# File lib/expedite/application.rb, line 69
def log(message)
  env.log "[application:#{variant}] #{message}"
end
preload() click to toggle source
# File lib/expedite/application.rb, line 93
def preload
  log "preloading app"

  @preloaded = :success
rescue Exception => e
  @preloaded = :failure
  raise e unless initialized?
end
preload_failed?() click to toggle source
# File lib/expedite/application.rb, line 77
def preload_failed?
  @preloaded == :failure
end
preloaded?() click to toggle source
# File lib/expedite/application.rb, line 73
def preloaded?
  @preloaded
end
print_exception(stream, error) click to toggle source
reset_streams() click to toggle source
# File lib/expedite/application.rb, line 273
def reset_streams
  [STDOUT, STDERR].each do |stream|
    stream.reopen(env.log_file)
  end
  STDIN.reopen("/dev/null")
end
run() click to toggle source
# File lib/expedite/application.rb, line 106
def run
  $0 = "expedite variant | #{app_name} | #{variant}"

  Expedite::Variants.lookup(variant).after_fork(variant)

  state :running
  manager.puts

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

    if terminating? || preload_failed?
      exit
    else
      serve manager.recv_io(UNIXSocket)
    end
  end
end
serve(client) click to toggle source
# File lib/expedite/application.rb, line 125
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")

  exec_name = args.shift
  command   = Expedite::Commands.lookup(exec_name)
  command.setup(client)

  connect_database

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

    ARGV.replace(args)
    $0 = exec_name

    # 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

  # Boot makes a new application, so we don't wait for it
  if command.is_a?(Expedite::Command::Boot)
    Process.detach(pid)
  else
    wait pid, streams, client
  end
rescue Exception => e
  log "exception: #{e} at #{e.backtrace.join("\n")}"
  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
shush_backtraces() click to toggle source

This feels very naughty

# File lib/expedite/application.rb, line 236
def shush_backtraces
  Kernel.module_eval do
    old_raise = Kernel.method(:raise)
    remove_method :raise
    define_method :raise do |*args|
      begin
        old_raise.call(*args)
      ensure
        if $!
          lib = File.expand_path("..", __FILE__)
          $!.backtrace.reject! { |line| line.start_with?(lib) }
        end
      end
    end
    private :raise
  end
end
state(val) click to toggle source
# File lib/expedite/application.rb, line 54
def state(val)
  return if exiting?
  log "#{@state} -> #{val}"
  @state = val
end
state!(val) click to toggle source
# File lib/expedite/application.rb, line 60
def state!(val)
  state val
  @interrupt.last.write "."
end
terminate() click to toggle source
# File lib/expedite/application.rb, line 199
def terminate
  if exiting?
    # Ensure that we do not ignore subsequent termination attempts
    log "forced exit"
    @waiting.each { |pid| Process.kill("TERM", pid) }
    Kernel.exit
  else
    state! :terminating
  end
end
terminating?() click to toggle source
# File lib/expedite/application.rb, line 85
def terminating?
  @state == :terminating
end
wait(pid, streams, client) click to toggle source
# File lib/expedite/application.rb, line 280
def wait(pid, streams, client)
  @mutex.synchronize { @waiting << pid }

  # Wait in a separate thread so we can run multiple commands at once
  Expedite.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
  }

  Expedite.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
with_pty() { || ... } click to toggle source
# File lib/expedite/application.rb, line 260
def with_pty
  PTY.open do |master, slave|
    [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
    reader_thread = Expedite.failsafe_thread { master.read }
    begin
      yield
    ensure
      reader_thread.kill
      reset_streams
    end
  end
end

Private Instance Methods

active_record_configured?() click to toggle source
# File lib/expedite/application.rb, line 312
def active_record_configured?
  defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
end