Skip to content

Commit d03a963

Browse files
committed
JRuby support: UNIXSocket#recv_io/send_io workaround, pooled application manager (instead of fork) - only used if fork if not supported
1 parent 354637f commit d03a963

20 files changed

+593
-98
lines changed

lib/spring/application.rb

+14-53
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
require "spring/boot"
22
require "set"
33
require "pty"
4+
require "spring/impl/application"
45

56
module Spring
67
class Application
8+
include ApplicationImpl
79
attr_reader :manager, :watcher, :spring_env, :original_env
810

911
def initialize(manager, original_env)
@@ -114,31 +116,28 @@ def preload
114116
end
115117
end
116118

117-
def eager_preload
118-
with_pty { preload }
119-
end
120-
121119
def run
122120
state :running
123-
manager.puts
121+
notify_manager_ready
124122

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

128126
if terminating? || watcher_stale? || preload_failed?
129127
exit
130128
else
131-
serve manager.recv_io(UNIXSocket)
129+
serve IOWrapper.recv_io(manager, UNIXSocket).to_io
132130
end
133131
end
134132
end
135133

136134
def serve(client)
135+
child_started = [false]
137136
log "got client"
138137
manager.puts
139138

140-
stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
141-
[STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
139+
stdout, stderr, stdin = streams = receive_streams(client)
140+
reopen_streams(streams)
142141

143142
preload unless preloaded?
144143

@@ -153,7 +152,7 @@ def serve(client)
153152
ActionDispatch::Reloader.prepare!
154153
end
155154

156-
pid = fork {
155+
fork_child(client, streams, child_started) {
157156
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
158157
trap("TERM", "DEFAULT")
159158

@@ -180,26 +179,21 @@ def serve(client)
180179
invoke_after_fork_callbacks
181180
shush_backtraces
182181

182+
before_command
183183
command.call
184184
}
185-
186-
disconnect_database
187-
reset_streams
188-
189-
log "forked #{pid}"
190-
manager.puts pid
191-
192-
wait pid, streams, client
193185
rescue Exception => e
186+
Kernel.exit if exiting? && e.is_a?(SystemExit)
187+
194188
log "exception: #{e}"
195-
manager.puts unless pid
189+
manager.puts unless child_started[0]
196190

197191
if streams && !e.is_a?(SystemExit)
198-
print_exception(stderr, e)
192+
print_exception(stderr || STDERR, e)
199193
streams.each(&:close)
200194
end
201195

202-
client.puts(1) if pid
196+
client.puts(1) if child_started[0]
203197
client.close
204198
end
205199

@@ -280,39 +274,6 @@ def print_exception(stream, error)
280274
rest.each { |line| stream.puts("\tfrom #{line}") }
281275
end
282276

283-
def with_pty
284-
PTY.open do |master, slave|
285-
[STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
286-
Thread.new { master.read }
287-
yield
288-
reset_streams
289-
end
290-
end
291-
292-
def reset_streams
293-
[STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
294-
STDIN.reopen("/dev/null")
295-
end
296-
297-
def wait(pid, streams, client)
298-
@mutex.synchronize { @waiting << pid }
299-
300-
# Wait in a separate thread so we can run multiple commands at once
301-
Thread.new {
302-
begin
303-
_, status = Process.wait2 pid
304-
log "#{pid} exited with #{status.exitstatus}"
305-
306-
streams.each(&:close)
307-
client.puts(status.exitstatus)
308-
client.close
309-
ensure
310-
@mutex.synchronize { @waiting.delete pid }
311-
exit_if_finished
312-
end
313-
}
314-
end
315-
316277
private
317278

318279
def active_record_configured?

lib/spring/application/boot.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
require "spring/platform"
12
# This is necessary for the terminal to work correctly when we reopen stdin.
2-
Process.setsid
3+
Process.setsid if Spring.fork?
34

45
require "spring/application"
56

67
app = Spring::Application.new(
7-
UNIXSocket.for_fd(3),
8+
Spring::WorkerChannel.remote_endpoint,
89
Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup)
910
)
1011

lib/spring/binstub.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
else
77
disable = ENV["DISABLE_SPRING"]
88

9-
if Process.respond_to?(:fork) && (disable.nil? || disable.empty? || disable == "0")
9+
if disable.nil? || disable.empty? || disable == "0"
1010
ARGV.unshift(command)
1111
load bin_path
1212
end

lib/spring/boot.rb

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
require "spring/process_title_updater"
77
require "spring/json"
88
require "spring/watcher"
9+
require "spring/io_helpers"

lib/spring/client/binstub.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Binstub < Command
4444
end
4545
CODE
4646

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

4949
class Item
5050
attr_reader :command, :existing

lib/spring/client/run.rb

+10-38
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
require "rbconfig"
22
require "socket"
33
require "bundler"
4+
require "spring/io_helpers"
5+
require "spring/impl/run"
46

57
module Spring
68
module Client
79
class Run < Command
8-
FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO) & Signal.list.keys
9-
TIMEOUT = 1
10+
include RunImpl
11+
TIMEOUT = RunImpl::TIMEOUT
1012

1113
def initialize(args)
1214
super
@@ -55,11 +57,11 @@ def cold_run
5557
def run
5658
verify_server_version
5759

58-
application, client = UNIXSocket.pair
60+
application, client = WorkerChannel.pair
5961

6062
queue_signals
6163
connect_to_application(client)
62-
run_command(client, application)
64+
run_command(client, application.to_io)
6365
end
6466

6567
def boot_server
@@ -108,7 +110,7 @@ def verify_server_version
108110
end
109111

110112
def connect_to_application(client)
111-
server.send_io client
113+
client.forward_to(server)
112114
send_json server, "args" => args, "default_rails_env" => default_rails_env
113115

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

124-
application.send_io STDOUT
125-
application.send_io STDERR
126-
application.send_io STDIN
126+
send_std_io_to(application)
127127

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

130+
IO.select([server])
130131
pid = server.gets
131132
pid = pid.chomp if pid
132133

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

141-
forward_signals(pid.to_i)
142-
status = application.read.to_i
143-
144-
log "got exit status #{status}"
145-
146-
exit status
142+
run_on(application, pid)
147143
else
148144
log "got no pid"
149145
exit 1
@@ -152,30 +148,6 @@ def run_command(client, application)
152148
application.close
153149
end
154150

155-
def queue_signals
156-
FORWARDED_SIGNALS.each do |sig|
157-
trap(sig) { @signal_queue << sig }
158-
end
159-
end
160-
161-
def forward_signals(pid)
162-
@signal_queue.each { |sig| kill sig, pid }
163-
164-
FORWARDED_SIGNALS.each do |sig|
165-
trap(sig) { forward_signal sig, pid }
166-
end
167-
rescue Errno::ESRCH
168-
end
169-
170-
def forward_signal(sig, pid)
171-
kill(sig, pid)
172-
rescue Errno::ESRCH
173-
# If the application process is gone, then don't block the
174-
# signal on this process.
175-
trap(sig, 'DEFAULT')
176-
Process.kill(sig, Process.pid)
177-
end
178-
179151
def kill(sig, pid)
180152
Process.kill(sig, -Process.getpgid(pid))
181153
end

lib/spring/configuration.rb

+8
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ def project_root_path
3737
@project_root_path ||= find_project_root(Pathname.new(File.expand_path(Dir.pwd)))
3838
end
3939

40+
def pool_min_free_workers
41+
2
42+
end
43+
44+
def pool_spawn_parallel
45+
true
46+
end
47+
4048
private
4149

4250
def find_project_root(current_dir)

lib/spring/env.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
require "spring/version"
77
require "spring/sid"
88
require "spring/configuration"
9+
require "spring/platform"
910

1011
module Spring
11-
IGNORE_SIGNALS = %w(INT QUIT)
1212
STOP_TIMEOUT = 2 # seconds
1313

1414
class Env

lib/spring/impl/application.rb

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "spring/platform"
2+
3+
if Spring.fork?
4+
require "spring/impl/fork/application"
5+
else
6+
require "spring/impl/pool/application"
7+
end
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "spring/platform"
2+
3+
if Spring.fork?
4+
require "spring/impl/fork/application_manager"
5+
else
6+
require "spring/impl/pool/application_manager"
7+
end

lib/spring/impl/fork/application.rb

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module Spring
2+
module ApplicationImpl
3+
def notify_manager_ready
4+
manager.puts
5+
end
6+
7+
def receive_streams(client)
8+
3.times.map { IOWrapper.recv_io(client).to_io }
9+
end
10+
11+
def reopen_streams(streams)
12+
[STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
13+
end
14+
15+
def eager_preload
16+
with_pty { preload }
17+
end
18+
19+
def with_pty
20+
PTY.open do |master, slave|
21+
[STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
22+
Thread.new { master.read }
23+
yield
24+
reset_streams
25+
end
26+
end
27+
28+
def reset_streams
29+
[STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
30+
STDIN.reopen("/dev/null")
31+
end
32+
33+
def wait(pid, streams, client)
34+
@mutex.synchronize { @waiting << pid }
35+
36+
# Wait in a separate thread so we can run multiple commands at once
37+
Thread.new {
38+
begin
39+
_, status = Process.wait2 pid
40+
log "#{pid} exited with #{status.exitstatus}"
41+
42+
streams.each(&:close)
43+
client.puts(status.exitstatus)
44+
client.close
45+
ensure
46+
@mutex.synchronize { @waiting.delete pid }
47+
exit_if_finished
48+
end
49+
}
50+
end
51+
52+
def fork_child(client, streams, child_started)
53+
pid = fork { yield }
54+
child_started[0] = true
55+
56+
disconnect_database
57+
reset_streams
58+
59+
log "forked #{pid}"
60+
manager.puts pid
61+
62+
wait pid, streams, client
63+
end
64+
65+
def before_command
66+
# NOP
67+
end
68+
end
69+
end

0 commit comments

Comments
 (0)