From 69fc4abb4e52e167c36bc6c8827329459765e1e7 Mon Sep 17 00:00:00 2001 From: Jan Berdajs Date: Tue, 24 Nov 2015 18:35:25 +0100 Subject: [PATCH 1/3] JRuby support: pooled application manager (instead of fork) - used if fork if not supported --- lib/spring/application.rb | 59 ++--- lib/spring/application/boot.rb | 9 +- lib/spring/application/fork_strategy.rb | 50 ++++ lib/spring/application/pool_strategy.rb | 30 +++ lib/spring/application_manager.rb | 139 +---------- .../application_manager/fork_strategy.rb | 140 +++++++++++ .../application_manager/pool_strategy.rb | 223 ++++++++++++++++++ lib/spring/binstub.rb | 2 +- lib/spring/client/run.rb | 6 +- lib/spring/configuration.rb | 8 + lib/spring/env.rb | 2 +- lib/spring/platform.rb | 25 ++ lib/spring/server.rb | 11 +- 13 files changed, 519 insertions(+), 185 deletions(-) create mode 100644 lib/spring/application/fork_strategy.rb create mode 100644 lib/spring/application/pool_strategy.rb create mode 100644 lib/spring/application_manager/fork_strategy.rb create mode 100644 lib/spring/application_manager/pool_strategy.rb create mode 100644 lib/spring/platform.rb diff --git a/lib/spring/application.rb b/lib/spring/application.rb index f10fc131..556d813b 100644 --- a/lib/spring/application.rb +++ b/lib/spring/application.rb @@ -1,9 +1,17 @@ require "spring/boot" require "set" require "pty" +require "spring/platform" module Spring class Application + if Spring.fork? + require 'spring/application/fork_strategy' + include ForkStrategy + else + require 'spring/application/pool_strategy' + include PoolStrategy + end attr_reader :manager, :watcher, :spring_env, :original_env def initialize(manager, original_env) @@ -114,13 +122,9 @@ def preload end end - def eager_preload - with_pty { preload } - end - def run state :running - manager.puts + manager.puts Process.pid loop do IO.select [manager, @interrupt.first] @@ -134,6 +138,7 @@ def run end def serve(client) + app_started = [false] log "got client" manager.puts @@ -153,7 +158,7 @@ def serve(client) ActionDispatch::Reloader.prepare! end - pid = fork { + start_app(client, streams, app_started) { IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") } trap("TERM", "DEFAULT") @@ -184,24 +189,18 @@ def serve(client) command.call } - - disconnect_database - reset_streams - - log "forked #{pid}" - manager.puts pid - - wait pid, streams, client rescue Exception => e + Kernel.exit if exiting? && e.is_a?(SystemExit) + log "exception: #{e}" - manager.puts unless pid + manager.puts unless app_started[0] if streams && !e.is_a?(SystemExit) print_exception(stderr, e) streams.each(&:close) end - client.puts(1) if pid + client.puts(1) if app_started[0] client.close end @@ -282,39 +281,11 @@ def print_exception(stream, error) rest.each { |line| stream.puts("\tfrom #{line}") } end - def with_pty - PTY.open do |master, slave| - [STDOUT, STDERR, STDIN].each { |s| s.reopen slave } - Thread.new { master.read } - yield - reset_streams - end - end - def reset_streams [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) } STDIN.reopen("/dev/null") end - def wait(pid, streams, client) - @mutex.synchronize { @waiting << pid } - - # Wait in a separate thread so we can run multiple commands at once - Thread.new { - 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 - } - end - private def active_record_configured? diff --git a/lib/spring/application/boot.rb b/lib/spring/application/boot.rb index 6804b646..21981dc0 100644 --- a/lib/spring/application/boot.rb +++ b/lib/spring/application/boot.rb @@ -3,8 +3,15 @@ require "spring/application" +remote_socket = + if ENV["SPRING_SOCKET"] + UNIXSocket.open(ENV.delete("SPRING_SOCKET")) + else + UNIXSocket.for_fd(3) + end + app = Spring::Application.new( - UNIXSocket.for_fd(3), + remote_socket, Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup) ) diff --git a/lib/spring/application/fork_strategy.rb b/lib/spring/application/fork_strategy.rb new file mode 100644 index 00000000..dc67789a --- /dev/null +++ b/lib/spring/application/fork_strategy.rb @@ -0,0 +1,50 @@ +module Spring + class Application + module ForkStrategy + def eager_preload + with_pty { preload } + end + + def with_pty + PTY.open do |master, slave| + [STDOUT, STDERR, STDIN].each { |s| s.reopen slave } + Thread.new { master.read } + yield + reset_streams + end + end + + def wait(pid, streams, client) + @mutex.synchronize { @waiting << pid } + + # Wait in a separate thread so we can run multiple commands at once + Thread.new { + 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 + } + end + + def start_app(client, streams, app_started) + pid = fork { yield } + app_started[0] = true + + disconnect_database + reset_streams + + log "forked #{pid}" + manager.puts pid + + wait pid, streams, client + end + end + end +end diff --git a/lib/spring/application/pool_strategy.rb b/lib/spring/application/pool_strategy.rb new file mode 100644 index 00000000..d4aad465 --- /dev/null +++ b/lib/spring/application/pool_strategy.rb @@ -0,0 +1,30 @@ +module Spring + class Application + module PoolStrategy + def eager_preload + reset_streams + preload + end + + def start_app(client, streams, app_started) + app_started[0] = true + exitstatus = 0 + manager.puts Process.pid + begin + log "started #{Process.pid}" + yield + rescue SystemExit => ex + exitstatus = ex.status + end + + log "#{Process.pid} exited with #{exitstatus}" + + streams.each(&:close) + client.puts(exitstatus) + client.close + + exit + end + end + end +end diff --git a/lib/spring/application_manager.rb b/lib/spring/application_manager.rb index a05aef4a..0bd9a560 100644 --- a/lib/spring/application_manager.rb +++ b/lib/spring/application_manager.rb @@ -1,138 +1,7 @@ module Spring - class ApplicationManager - attr_reader :pid, :child, :app_env, :spring_env, :status - - def initialize(app_env) - @app_env = app_env - @spring_env = Env.new - @mutex = Mutex.new - @state = :running - end - - def log(message) - spring_env.log "[application_manager:#{app_env}] #{message}" - end - - # We're not using @mutex.synchronize to avoid the weird ":10" - # line which messes with backtraces in e.g. rspec - def synchronize - @mutex.lock - yield - ensure - @mutex.unlock - end - - def start - start_child - end - - def restart - return if @state == :stopping - start_child(true) - end - - def alive? - @pid - 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 - - # Returns the pid of the process running the command, or nil if the application process died. - 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 stop - log "stopping" - @state = :stopping - - if pid - Process.kill('TERM', pid) - Process.wait(pid) - end - rescue Errno::ESRCH, Errno::ECHILD - # Don't care - end - - private - - 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 - ) - end - - start_wait_thread(pid, child) if child.gets - child_socket.close - end - - def start_wait_thread(pid, child) - Process.detach(pid) - - Thread.new { - # 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 + module ApplicationManager end end + +require 'spring/application_manager/fork_strategy' +require 'spring/application_manager/pool_strategy' diff --git a/lib/spring/application_manager/fork_strategy.rb b/lib/spring/application_manager/fork_strategy.rb new file mode 100644 index 00000000..5730be17 --- /dev/null +++ b/lib/spring/application_manager/fork_strategy.rb @@ -0,0 +1,140 @@ +module Spring + module ApplicationManager + class ForkStrategy + attr_reader :pid, :child, :app_env, :spring_env, :status + + def initialize(app_env) + @app_env = app_env + @spring_env = Env.new + @mutex = Mutex.new + @state = :running + end + + def log(message) + spring_env.log "[application_manager:#{app_env}] #{message}" + end + + # We're not using @mutex.synchronize to avoid the weird ":10" + # line which messes with backtraces in e.g. rspec + def synchronize + @mutex.lock + yield + ensure + @mutex.unlock + end + + def start + start_child + end + + def restart + return if @state == :stopping + start_child(true) + end + + def alive? + @pid + 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 + + # Returns the pid of the process running the command, or nil if the application process died. + 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 stop + log "stopping" + @state = :stopping + + if pid + Process.kill('TERM', pid) + Process.wait(pid) + end + rescue Errno::ESRCH, Errno::ECHILD + # Don't care + end + + private + + 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 + ) + end + + start_wait_thread(pid, child) if child.gets + child_socket.close + end + + def start_wait_thread(pid, child) + Process.detach(pid) + + Thread.new { + # 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 + end + end +end diff --git a/lib/spring/application_manager/pool_strategy.rb b/lib/spring/application_manager/pool_strategy.rb new file mode 100644 index 00000000..d2d88ba1 --- /dev/null +++ b/lib/spring/application_manager/pool_strategy.rb @@ -0,0 +1,223 @@ +require 'securerandom' + +module Spring + module ApplicationManager + class PoolStrategy + class Worker + attr_reader :pid, :socket + attr_accessor :on_done + + def initialize(env, args) + @spring_env = Env.new + path = @spring_env.tmp_path.join("#{SecureRandom.uuid}.sock").to_s + @server = UNIXServer.open(path) + + Bundler.with_clean_env do + @pid = + Process.spawn(env.merge("SPRING_SOCKET" => path), *args) + log "worker spawned" + end + + @socket = @server.accept + end + + def await_boot + @pid = socket.gets.to_i + Process.detach(pid) + @wait_thread = start_wait_thread(pid, socket) unless pid.zero? + end + + def kill(sig = 9) + Process.kill(sig, pid) + rescue Errno::ESRCH + end + + def join + @wait_thread.join if @wait_thread + end + + protected + + def start_wait_thread(pid, child) + Thread.new do + begin + Process.wait pid + rescue Errno::ECHILD + # Not sure why this gets raised + rescue StandardError => e + log "error waiting for worker: #{e.class}: #{e.message}" + end + + kill + log "child #{pid} shutdown" + + on_done.call(self) if on_done + end + end + + def log(message) + @spring_env.log "[worker:#{pid}] #{message}" + end + end + + class WorkerPool + def initialize(app_env, *app_args) + @app_env = app_env + @app_args = app_args + @spring_env = Env.new + + @workers = [] + @workers_in_use = [] + @spawning_workers = [] + + @check_mutex = Mutex.new + @workers_mutex = Mutex.new + + run + end + + def add_worker + worker = Worker.new(@app_env, @app_args) + worker.on_done = method(:worker_done) + @workers_mutex.synchronize { @spawning_workers << worker } + Thread.new do + worker.await_boot + log "+ worker #{worker.pid}" + @workers_mutex.synchronize do + @spawning_workers.delete(worker) + @workers << worker + end + end + end + + def worker_done(worker) + log "- worker #{worker.pid}" + @workers_mutex.synchronize do + @workers_in_use.delete(worker) + end + end + + def get_worker(spawn_new = true) + add_worker if spawn_new && all_size == 0 + + worker = nil + while worker.nil? && all_size > 0 + @workers_mutex.synchronize do + worker = @workers.shift + @workers_in_use << worker if worker + end + break if worker + sleep 1 + end + + Thread.new { check_min_free_workers } if spawn_new + + worker + end + + def check_min_free_workers + if @check_mutex.try_lock + while all_size < Spring.pool_min_free_workers + unless Spring.pool_spawn_parallel + sleep 0.1 until @workers_mutex.synchronize { @spawning_workers.empty? } + end + add_worker + end + @check_mutex.unlock + end + end + + def all_size + @workers_mutex.synchronize { @workers.size + @spawning_workers.size } + end + + def stop! + if spawning_worker_pids.include?(nil) + log "Waiting for workers to quit..." + sleep 0.1 while spawning_worker_pids.include?(nil) + end + + waiting_workers = + @workers_mutex.synchronize do + (@spawning_workers + @workers_in_use + @workers).each(&:kill) + end + waiting_workers.each(&:join) + end + + protected + + def spawning_worker_pids + @spawning_workers.map { |worker| worker.pid } + end + + def run + check_min_free_workers + end + + def log(message) + @spring_env.log "[worker:pool] #{message}" + end + end + + def initialize(app_env) + @app_env = app_env + @spring_env = Env.new + @pool = + WorkerPool.new( + { + "RAILS_ENV" => app_env, + "RACK_ENV" => app_env, + "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV), + "SPRING_PRELOAD" => "1", + }, + Spring.ruby_bin, + "-I", File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first), + "-I", File.expand_path("../..", __FILE__), + "-e", "require 'spring/application/boot'" + ) + end + + # Returns the name of the screen running the command, or nil if the application process died. + def run(client) + pid = nil + with_child do |child| + child.socket.send_io(client) + IO.select([child.socket]) + child.socket.gets or raise Errno::EPIPE + IO.select([child.socket]) + pid = child.socket.gets.to_i + end + + 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 stop + log "stopping" + + @pool.stop! + rescue Errno::ESRCH, Errno::ECHILD + # Don't care + end + + protected + + attr_reader :app_env, :spring_env + + def log(message) + spring_env.log "[application_manager:#{app_env}] #{message}" + end + + def with_child + yield(@pool.get_worker) + end + end + end +end diff --git a/lib/spring/binstub.rb b/lib/spring/binstub.rb index 75f92fb4..ec40aa25 100644 --- a/lib/spring/binstub.rb +++ b/lib/spring/binstub.rb @@ -6,7 +6,7 @@ else disable = ENV["DISABLE_SPRING"] - if Process.respond_to?(:fork) && (disable.nil? || disable.empty? || disable == "0") + if disable.nil? || disable.empty? || disable == "0" ARGV.unshift(command) load bin_path end diff --git a/lib/spring/client/run.rb b/lib/spring/client/run.rb index 9d994541..7431b618 100644 --- a/lib/spring/client/run.rb +++ b/lib/spring/client/run.rb @@ -1,12 +1,12 @@ require "rbconfig" require "socket" require "bundler" +require "spring/platform" module Spring module Client class Run < Command - FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys - TIMEOUT = 1 + TIMEOUT = Spring.fork? ? 1 : 60 def initialize(args) super @@ -127,6 +127,7 @@ def run_command(client, application) send_json application, "args" => args, "env" => ENV.to_hash + IO.select([server]) pid = server.gets pid = pid.chomp if pid @@ -139,6 +140,7 @@ def run_command(client, application) log "got pid: #{pid}" forward_signals(pid.to_i) + IO.select([application]) status = application.read.to_i log "got exit status #{status}" diff --git a/lib/spring/configuration.rb b/lib/spring/configuration.rb index 103f0882..24695044 100644 --- a/lib/spring/configuration.rb +++ b/lib/spring/configuration.rb @@ -37,6 +37,14 @@ def project_root_path @project_root_path ||= find_project_root(Pathname.new(File.expand_path(Dir.pwd))) end + def pool_min_free_workers + 2 + end + + def pool_spawn_parallel + true + end + private def find_project_root(current_dir) diff --git a/lib/spring/env.rb b/lib/spring/env.rb index 6a60f0ae..1a481839 100644 --- a/lib/spring/env.rb +++ b/lib/spring/env.rb @@ -6,9 +6,9 @@ require "spring/version" require "spring/sid" require "spring/configuration" +require "spring/platform" module Spring - IGNORE_SIGNALS = %w(INT QUIT) STOP_TIMEOUT = 2 # seconds class Env diff --git a/lib/spring/platform.rb b/lib/spring/platform.rb new file mode 100644 index 00000000..5b3b122b --- /dev/null +++ b/lib/spring/platform.rb @@ -0,0 +1,25 @@ +module Spring + def self.fork? + Process.respond_to?(:fork) + end + + def self.jruby? + RUBY_PLATFORM == "java" + end + + def self.ruby_bin + if RUBY_PLATFORM == "java" + "jruby" + else + "ruby" + end + end + + if jruby? + IGNORE_SIGNALS = %w(INT) + FORWARDED_SIGNALS = %w(INT USR2 INFO WINCH) & Signal.list.keys + else + IGNORE_SIGNALS = %w(INT QUIT) + FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys + end +end diff --git a/lib/spring/server.rb b/lib/spring/server.rb index b922199f..8d17f30f 100644 --- a/lib/spring/server.rb +++ b/lib/spring/server.rb @@ -3,6 +3,7 @@ module Spring end require "spring/boot" +require "spring/platform" require "spring/application_manager" # Must be last, as it requires bundler/setup, which alters the load path @@ -18,7 +19,7 @@ def self.boot def initialize(env = Env.new) @env = env - @applications = Hash.new { |h, k| h[k] = ApplicationManager.new(k) } + @applications = Hash.new { |h, k| h[k] = new_application_manager(k) } @pidfile = env.pidfile_path.open('a') @mutex = Mutex.new end @@ -126,5 +127,13 @@ def set_process_title "spring server | #{env.app_name} | started #{distance} ago" } end + + def new_application_manager(app_env) + if Spring.fork? + ApplicationManager::ForkStrategy.new(app_env) + else + ApplicationManager::PoolStrategy.new(app_env) + end + end end end From 514dc0f76efe72a5f61f75000b46a9caf05dca68 Mon Sep 17 00:00:00 2001 From: Jan Berdajs Date: Sat, 6 Feb 2016 14:33:11 +0100 Subject: [PATCH 2/3] refactor --- lib/spring/application.rb | 293 +----------------------- lib/spring/application/base.rb | 290 +++++++++++++++++++++++ lib/spring/application/boot.rb | 2 +- lib/spring/application/fork_strategy.rb | 8 +- lib/spring/application/pool_strategy.rb | 8 +- lib/spring/platform.rb | 4 +- 6 files changed, 308 insertions(+), 297 deletions(-) create mode 100644 lib/spring/application/base.rb diff --git a/lib/spring/application.rb b/lib/spring/application.rb index 556d813b..9cd80294 100644 --- a/lib/spring/application.rb +++ b/lib/spring/application.rb @@ -2,294 +2,15 @@ require "set" require "pty" require "spring/platform" +require "spring/application/base" +require "spring/application/pool_strategy" +require "spring/application/fork_strategy" module Spring - class Application - if Spring.fork? - require 'spring/application/fork_strategy' - include ForkStrategy - else - require 'spring/application/pool_strategy' - include PoolStrategy - end - attr_reader :manager, :watcher, :spring_env, :original_env - - def initialize(manager, original_env) - @manager = manager - @original_env = original_env - @spring_env = Env.new - @mutex = Mutex.new - @waiting = Set.new - @preloaded = false - @state = :initialized - @interrupt = IO.pipe - end - - def state(val) - return if exiting? - log "#{@state} -> #{val}" - @state = val - end - - def state!(val) - state val - @interrupt.last.write "." - end - - def app_env - ENV['RAILS_ENV'] - end - - def app_name - spring_env.app_name - end - - def log(message) - spring_env.log "[application:#{app_env}] #{message}" - end - - def preloaded? - @preloaded - end - - def preload_failed? - @preloaded == :failure - end - - def exiting? - @state == :exiting - end - - def terminating? - @state == :terminating - end - - def watcher_stale? - @state == :watcher_stale - end - - def initialized? - @state == :initialized - end - - def start_watcher - @watcher = Spring.watcher - @watcher.on_stale { state! :watcher_stale } - @watcher.start - end - - def preload - log "preloading app" - - begin - require "spring/commands" - ensure - start_watcher - end - - require Spring.application_root_path.join("config", "application") - - # config/environments/test.rb will have config.cache_classes = true. However - # we want it to be false so that we can reload files. This is a hack to - # override the effect of config.cache_classes = true. We can then actually - # set config.cache_classes = false after loading the environment. - Rails::Application.initializer :initialize_dependency_mechanism, group: :all do - ActiveSupport::Dependencies.mechanism = :load - end - - require Spring.application_root_path.join("config", "environment") - - @original_cache_classes = Rails.application.config.cache_classes - Rails.application.config.cache_classes = false - - disconnect_database - - @preloaded = :success - rescue Exception => e - @preloaded = :failure - watcher.add e.backtrace.map { |line| line[/^(.*)\:\d+/, 1] } - raise e unless initialized? - ensure - watcher.add loaded_application_features - watcher.add Spring.gemfile, "#{Spring.gemfile}.lock" - - if defined?(Rails) && Rails.application - watcher.add Rails.application.paths["config/initializers"] - watcher.add Rails.application.paths["config/database"] - if secrets_path = Rails.application.paths["config/secrets"] - watcher.add secrets_path - end - end - end - - def run - state :running - manager.puts Process.pid - - 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) - app_started = [false] - 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?) - ActionDispatch::Reloader.cleanup! - ActionDispatch::Reloader.prepare! - end - - start_app(client, streams, app_started) { - 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 - } - rescue Exception => e - Kernel.exit if exiting? && e.is_a?(SystemExit) - - log "exception: #{e}" - manager.puts unless app_started[0] - - if streams && !e.is_a?(SystemExit) - print_exception(stderr, e) - streams.each(&:close) - end - - client.puts(1) if app_started[0] - client.close - end - - 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 - - def exit - state :exiting - manager.shutdown(:RDWR) - exit_if_finished - sleep - end - - def exit_if_finished - @mutex.synchronize { - Kernel.exit if exiting? && @waiting.empty? - } - end - - # The command might need to require some files in the - # main process so that they are cached. For example a test command wants to - # load the helper file once and have it cached. - def setup(command) - if command.setup - watcher.add loaded_application_features # loaded features may have changed - end - end - - def invoke_after_fork_callbacks - Spring.after_fork_callbacks.each do |callback| - callback.call - end - end - - def loaded_application_features - root = Spring.application_root_path.to_s - $LOADED_FEATURES.select { |f| f.start_with?(root) } - end - - def disconnect_database - ActiveRecord::Base.remove_connection if active_record_configured? - end - - def connect_database - ActiveRecord::Base.establish_connection if active_record_configured? - end - - # This feels very naughty - 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 - - def print_exception(stream, error) - first, rest = error.backtrace.first, error.backtrace.drop(1) - stream.puts("#{first}: #{error} (#{error.class})") - rest.each { |line| stream.puts("\tfrom #{line}") } - end - - def reset_streams - [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) } - STDIN.reopen("/dev/null") - end - - private - - def active_record_configured? - defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any? + module Application + def self.create(*args) + strategy = Spring.fork? ? ForkStrategy : PoolStrategy + strategy.new(*args) end end end diff --git a/lib/spring/application/base.rb b/lib/spring/application/base.rb new file mode 100644 index 00000000..efcd3bac --- /dev/null +++ b/lib/spring/application/base.rb @@ -0,0 +1,290 @@ +require "spring/boot" +require "set" +require "pty" +require "spring/platform" + +module Spring + module Application + class Base + attr_reader :manager, :watcher, :spring_env, :original_env + + def initialize(manager, original_env) + @manager = manager + @original_env = original_env + @spring_env = Env.new + @mutex = Mutex.new + @waiting = Set.new + @preloaded = false + @state = :initialized + @interrupt = IO.pipe + end + + def state(val) + return if exiting? + log "#{@state} -> #{val}" + @state = val + end + + def state!(val) + state val + @interrupt.last.write "." + end + + def app_env + ENV['RAILS_ENV'] + end + + def app_name + spring_env.app_name + end + + def log(message) + spring_env.log "[application:#{app_env}] #{message}" + end + + def preloaded? + @preloaded + end + + def preload_failed? + @preloaded == :failure + end + + def exiting? + @state == :exiting + end + + def terminating? + @state == :terminating + end + + def watcher_stale? + @state == :watcher_stale + end + + def initialized? + @state == :initialized + end + + def start_watcher + @watcher = Spring.watcher + @watcher.on_stale { state! :watcher_stale } + @watcher.start + end + + def preload + log "preloading app" + + begin + require "spring/commands" + ensure + start_watcher + end + + require Spring.application_root_path.join("config", "application") + + # config/environments/test.rb will have config.cache_classes = true. However + # we want it to be false so that we can reload files. This is a hack to + # override the effect of config.cache_classes = true. We can then actually + # set config.cache_classes = false after loading the environment. + Rails::Application.initializer :initialize_dependency_mechanism, group: :all do + ActiveSupport::Dependencies.mechanism = :load + end + + require Spring.application_root_path.join("config", "environment") + + @original_cache_classes = Rails.application.config.cache_classes + Rails.application.config.cache_classes = false + + disconnect_database + + @preloaded = :success + rescue Exception => e + @preloaded = :failure + watcher.add e.backtrace.map { |line| line[/^(.*)\:\d+/, 1] } + raise e unless initialized? + ensure + watcher.add loaded_application_features + watcher.add Spring.gemfile, "#{Spring.gemfile}.lock" + + if defined?(Rails) && Rails.application + watcher.add Rails.application.paths["config/initializers"] + watcher.add Rails.application.paths["config/database"] + if secrets_path = Rails.application.paths["config/secrets"] + watcher.add secrets_path + end + end + end + + def run + state :running + manager.puts Process.pid + + 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) + app_status = { started: false } + 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?) + ActionDispatch::Reloader.cleanup! + ActionDispatch::Reloader.prepare! + end + + start_app(client, streams, app_status) { + 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 + } + rescue Exception => e + Kernel.exit if exiting? && e.is_a?(SystemExit) + + log "exception: #{e}" + manager.puts unless app_status[:started] + + if streams && !e.is_a?(SystemExit) + print_exception(stderr, e) + streams.each(&:close) + end + + client.puts(1) if app_status[:started] + client.close + end + + 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 + + def exit + state :exiting + manager.shutdown(:RDWR) + exit_if_finished + sleep + end + + def exit_if_finished + @mutex.synchronize { + Kernel.exit if exiting? && @waiting.empty? + } + end + + # The command might need to require some files in the + # main process so that they are cached. For example a test command wants to + # load the helper file once and have it cached. + def setup(command) + if command.setup + watcher.add loaded_application_features # loaded features may have changed + end + end + + def invoke_after_fork_callbacks + Spring.after_fork_callbacks.each do |callback| + callback.call + end + end + + def loaded_application_features + root = Spring.application_root_path.to_s + $LOADED_FEATURES.select { |f| f.start_with?(root) } + end + + def disconnect_database + ActiveRecord::Base.remove_connection if active_record_configured? + end + + def connect_database + ActiveRecord::Base.establish_connection if active_record_configured? + end + + # This feels very naughty + 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 + + def print_exception(stream, error) + first, rest = error.backtrace.first, error.backtrace.drop(1) + stream.puts("#{first}: #{error} (#{error.class})") + rest.each { |line| stream.puts("\tfrom #{line}") } + end + + def reset_streams + [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) } + STDIN.reopen("/dev/null") + end + + private + + def active_record_configured? + defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any? + end + end + end +end diff --git a/lib/spring/application/boot.rb b/lib/spring/application/boot.rb index 21981dc0..3f5d4f6f 100644 --- a/lib/spring/application/boot.rb +++ b/lib/spring/application/boot.rb @@ -10,7 +10,7 @@ UNIXSocket.for_fd(3) end -app = Spring::Application.new( +app = Spring::Application.create( remote_socket, Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup) ) diff --git a/lib/spring/application/fork_strategy.rb b/lib/spring/application/fork_strategy.rb index dc67789a..307ae38a 100644 --- a/lib/spring/application/fork_strategy.rb +++ b/lib/spring/application/fork_strategy.rb @@ -1,6 +1,6 @@ module Spring - class Application - module ForkStrategy + module Application + class ForkStrategy < Base def eager_preload with_pty { preload } end @@ -33,9 +33,9 @@ def wait(pid, streams, client) } end - def start_app(client, streams, app_started) + def start_app(client, streams, app_status) pid = fork { yield } - app_started[0] = true + app_status[:started] = true disconnect_database reset_streams diff --git a/lib/spring/application/pool_strategy.rb b/lib/spring/application/pool_strategy.rb index d4aad465..50c4bd18 100644 --- a/lib/spring/application/pool_strategy.rb +++ b/lib/spring/application/pool_strategy.rb @@ -1,13 +1,13 @@ module Spring - class Application - module PoolStrategy + module Application + class PoolStrategy < Base def eager_preload reset_streams preload end - def start_app(client, streams, app_started) - app_started[0] = true + def start_app(client, streams, app_status) + app_status[:started] = true exitstatus = 0 manager.puts Process.pid begin diff --git a/lib/spring/platform.rb b/lib/spring/platform.rb index 5b3b122b..9289522d 100644 --- a/lib/spring/platform.rb +++ b/lib/spring/platform.rb @@ -4,11 +4,11 @@ def self.fork? end def self.jruby? - RUBY_PLATFORM == "java" + RUBY_ENGINE == "jruby" end def self.ruby_bin - if RUBY_PLATFORM == "java" + if RUBY_ENGINE == "jruby" "jruby" else "ruby" From e98ea1b4eeeee5dd1574903b9c1be1fcf4de7387 Mon Sep 17 00:00:00 2001 From: Jan Berdajs Date: Sat, 6 Feb 2016 14:47:19 +0100 Subject: [PATCH 3/3] don't use spring if running on Windows --- lib/spring/binstub.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/spring/binstub.rb b/lib/spring/binstub.rb index ec40aa25..48a6d6c4 100644 --- a/lib/spring/binstub.rb +++ b/lib/spring/binstub.rb @@ -1,3 +1,5 @@ +require 'rbconfig' + command = File.basename($0) bin_path = File.expand_path("../../../bin/spring", __FILE__) @@ -5,8 +7,9 @@ load bin_path else disable = ENV["DISABLE_SPRING"] + not_windows = !(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) - if disable.nil? || disable.empty? || disable == "0" + if not_windows && (disable.nil? || disable.empty? || disable == "0") ARGV.unshift(command) load bin_path end