Skip to content

Commit

Permalink
JRuby support: UNIXSocket#recv_io/send_io workaround, pooled applicat…
Browse files Browse the repository at this point in the history
…ion manager (instead of fork) - only used if fork if not supported
  • Loading branch information
mrbrdo committed Nov 17, 2015
1 parent 354637f commit d03a963
Show file tree
Hide file tree
Showing 20 changed files with 593 additions and 98 deletions.
67 changes: 14 additions & 53 deletions lib/spring/application.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
require "spring/boot"
require "set"
require "pty"
require "spring/impl/application"

module Spring
class Application
include ApplicationImpl
attr_reader :manager, :watcher, :spring_env, :original_env

def initialize(manager, original_env)
Expand Down Expand Up @@ -114,31 +116,28 @@ def preload
end
end

def eager_preload
with_pty { preload }
end

def run
state :running
manager.puts
notify_manager_ready

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

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

def serve(client)
child_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) }
stdout, stderr, stdin = streams = receive_streams(client)
reopen_streams(streams)

preload unless preloaded?

Expand All @@ -153,7 +152,7 @@ def serve(client)
ActionDispatch::Reloader.prepare!
end

pid = fork {
fork_child(client, streams, child_started) {
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
trap("TERM", "DEFAULT")

Expand All @@ -180,26 +179,21 @@ def serve(client)
invoke_after_fork_callbacks
shush_backtraces

before_command
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 child_started[0]

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

client.puts(1) if pid
client.puts(1) if child_started[0]
client.close
end

Expand Down Expand Up @@ -280,39 +274,6 @@ 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?
Expand Down
5 changes: 3 additions & 2 deletions lib/spring/application/boot.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
require "spring/platform"
# This is necessary for the terminal to work correctly when we reopen stdin.
Process.setsid
Process.setsid if Spring.fork?

require "spring/application"

app = Spring::Application.new(
UNIXSocket.for_fd(3),
Spring::WorkerChannel.remote_endpoint,
Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup)
)

Expand Down
2 changes: 1 addition & 1 deletion lib/spring/binstub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/spring/boot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
require "spring/process_title_updater"
require "spring/json"
require "spring/watcher"
require "spring/io_helpers"
2 changes: 1 addition & 1 deletion lib/spring/client/binstub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Binstub < Command
end
CODE

OLD_BINSTUB = %{if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty?}
OLD_BINSTUB = %{if Gem::Specification.find_all_by_name("spring").empty?}

class Item
attr_reader :command, :existing
Expand Down
48 changes: 10 additions & 38 deletions lib/spring/client/run.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
require "rbconfig"
require "socket"
require "bundler"
require "spring/io_helpers"
require "spring/impl/run"

module Spring
module Client
class Run < Command
FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO) & Signal.list.keys
TIMEOUT = 1
include RunImpl
TIMEOUT = RunImpl::TIMEOUT

def initialize(args)
super
Expand Down Expand Up @@ -55,11 +57,11 @@ def cold_run
def run
verify_server_version

application, client = UNIXSocket.pair
application, client = WorkerChannel.pair

queue_signals
connect_to_application(client)
run_command(client, application)
run_command(client, application.to_io)
end

def boot_server
Expand Down Expand Up @@ -108,7 +110,7 @@ def verify_server_version
end

def connect_to_application(client)
server.send_io client
client.forward_to(server)
send_json server, "args" => args, "default_rails_env" => default_rails_env

if IO.select([server], [], [], TIMEOUT)
Expand All @@ -121,12 +123,11 @@ def connect_to_application(client)
def run_command(client, application)
log "sending command"

application.send_io STDOUT
application.send_io STDERR
application.send_io STDIN
send_std_io_to(application)

send_json application, "args" => args, "env" => ENV.to_hash

IO.select([server])
pid = server.gets
pid = pid.chomp if pid

Expand All @@ -138,12 +139,7 @@ def run_command(client, application)
if pid && !pid.empty?
log "got pid: #{pid}"

forward_signals(pid.to_i)
status = application.read.to_i

log "got exit status #{status}"

exit status
run_on(application, pid)
else
log "got no pid"
exit 1
Expand All @@ -152,30 +148,6 @@ def run_command(client, application)
application.close
end

def queue_signals
FORWARDED_SIGNALS.each do |sig|
trap(sig) { @signal_queue << sig }
end
end

def forward_signals(pid)
@signal_queue.each { |sig| kill sig, pid }

FORWARDED_SIGNALS.each do |sig|
trap(sig) { forward_signal sig, pid }
end
rescue Errno::ESRCH
end

def forward_signal(sig, pid)
kill(sig, pid)
rescue Errno::ESRCH
# If the application process is gone, then don't block the
# signal on this process.
trap(sig, 'DEFAULT')
Process.kill(sig, Process.pid)
end

def kill(sig, pid)
Process.kill(sig, -Process.getpgid(pid))
end
Expand Down
8 changes: 8 additions & 0 deletions lib/spring/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/spring/env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lib/spring/impl/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "spring/platform"

if Spring.fork?
require "spring/impl/fork/application"
else
require "spring/impl/pool/application"
end
7 changes: 7 additions & 0 deletions lib/spring/impl/application_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "spring/platform"

if Spring.fork?
require "spring/impl/fork/application_manager"
else
require "spring/impl/pool/application_manager"
end
69 changes: 69 additions & 0 deletions lib/spring/impl/fork/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Spring
module ApplicationImpl
def notify_manager_ready
manager.puts
end

def receive_streams(client)
3.times.map { IOWrapper.recv_io(client).to_io }
end

def reopen_streams(streams)
[STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
end

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

def fork_child(client, streams, child_started)
pid = fork { yield }
child_started[0] = true

disconnect_database
reset_streams

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

wait pid, streams, client
end

def before_command
# NOP
end
end
end
File renamed without changes.
Loading

0 comments on commit d03a963

Please sign in to comment.