Skip to content

Commit f591a30

Browse files
committed
JRuby support: pooled application manager (instead of fork) - used if fork if not supported
1 parent 354637f commit f591a30

13 files changed

+517
-184
lines changed

lib/spring/application.rb

+15-44
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
require "spring/boot"
22
require "set"
33
require "pty"
4+
require "spring/platform"
45

56
module Spring
67
class Application
8+
if Spring.fork?
9+
require 'spring/application/fork_strategy'
10+
include ForkStrategy
11+
else
12+
require 'spring/application/pool_strategy'
13+
include PoolStrategy
14+
end
715
attr_reader :manager, :watcher, :spring_env, :original_env
816

917
def initialize(manager, original_env)
@@ -114,13 +122,9 @@ def preload
114122
end
115123
end
116124

117-
def eager_preload
118-
with_pty { preload }
119-
end
120-
121125
def run
122126
state :running
123-
manager.puts
127+
manager.puts Process.pid
124128

125129
loop do
126130
IO.select [manager, @interrupt.first]
@@ -134,6 +138,7 @@ def run
134138
end
135139

136140
def serve(client)
141+
app_started = [false]
137142
log "got client"
138143
manager.puts
139144

@@ -153,7 +158,7 @@ def serve(client)
153158
ActionDispatch::Reloader.prepare!
154159
end
155160

156-
pid = fork {
161+
start_app(client, streams, app_started) {
157162
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
158163
trap("TERM", "DEFAULT")
159164

@@ -182,24 +187,18 @@ def serve(client)
182187

183188
command.call
184189
}
185-
186-
disconnect_database
187-
reset_streams
188-
189-
log "forked #{pid}"
190-
manager.puts pid
191-
192-
wait pid, streams, client
193190
rescue Exception => e
191+
Kernel.exit if exiting? && e.is_a?(SystemExit)
192+
194193
log "exception: #{e}"
195-
manager.puts unless pid
194+
manager.puts unless app_started[0]
196195

197196
if streams && !e.is_a?(SystemExit)
198197
print_exception(stderr, e)
199198
streams.each(&:close)
200199
end
201200

202-
client.puts(1) if pid
201+
client.puts(1) if app_started[0]
203202
client.close
204203
end
205204

@@ -280,39 +279,11 @@ def print_exception(stream, error)
280279
rest.each { |line| stream.puts("\tfrom #{line}") }
281280
end
282281

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-
292282
def reset_streams
293283
[STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
294284
STDIN.reopen("/dev/null")
295285
end
296286

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-
316287
private
317288

318289
def active_record_configured?

lib/spring/application/boot.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33

44
require "spring/application"
55

6+
remote_socket =
7+
if ENV["SPRING_SOCKET"]
8+
UNIXSocket.open(ENV.delete("SPRING_SOCKET"))
9+
else
10+
UNIXSocket.for_fd(3)
11+
end
12+
613
app = Spring::Application.new(
7-
UNIXSocket.for_fd(3),
14+
remote_socket,
815
Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup)
916
)
1017

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module Spring
2+
class Application
3+
module ForkStrategy
4+
def eager_preload
5+
with_pty { preload }
6+
end
7+
8+
def with_pty
9+
PTY.open do |master, slave|
10+
[STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
11+
Thread.new { master.read }
12+
yield
13+
reset_streams
14+
end
15+
end
16+
17+
def wait(pid, streams, client)
18+
@mutex.synchronize { @waiting << pid }
19+
20+
# Wait in a separate thread so we can run multiple commands at once
21+
Thread.new {
22+
begin
23+
_, status = Process.wait2 pid
24+
log "#{pid} exited with #{status.exitstatus}"
25+
26+
streams.each(&:close)
27+
client.puts(status.exitstatus)
28+
client.close
29+
ensure
30+
@mutex.synchronize { @waiting.delete pid }
31+
exit_if_finished
32+
end
33+
}
34+
end
35+
36+
def start_app(client, streams, app_started)
37+
pid = fork { yield }
38+
app_started[0] = true
39+
40+
disconnect_database
41+
reset_streams
42+
43+
log "forked #{pid}"
44+
manager.puts pid
45+
46+
wait pid, streams, client
47+
end
48+
end
49+
end
50+
end
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module Spring
2+
class Application
3+
module PoolStrategy
4+
def eager_preload
5+
reset_streams
6+
preload
7+
end
8+
9+
def start_app(client, streams, app_started)
10+
app_started[0] = true
11+
exitstatus = 0
12+
manager.puts Process.pid
13+
begin
14+
log "started #{Process.pid}"
15+
yield
16+
rescue SystemExit => ex
17+
exitstatus = ex.status
18+
end
19+
20+
log "#{Process.pid} exited with #{exitstatus}"
21+
22+
streams.each(&:close)
23+
client.puts(exitstatus)
24+
client.close
25+
26+
exit
27+
end
28+
end
29+
end
30+
end

lib/spring/application_manager.rb

+4-134
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,7 @@
11
module Spring
2-
class ApplicationManager
3-
attr_reader :pid, :child, :app_env, :spring_env, :status
4-
5-
def initialize(app_env)
6-
@app_env = app_env
7-
@spring_env = Env.new
8-
@mutex = Mutex.new
9-
@state = :running
10-
end
11-
12-
def log(message)
13-
spring_env.log "[application_manager:#{app_env}] #{message}"
14-
end
15-
16-
# We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
17-
# line which messes with backtraces in e.g. rspec
18-
def synchronize
19-
@mutex.lock
20-
yield
21-
ensure
22-
@mutex.unlock
23-
end
24-
25-
def start
26-
start_child
27-
end
28-
29-
def restart
30-
return if @state == :stopping
31-
start_child(true)
32-
end
33-
34-
def alive?
35-
@pid
36-
end
37-
38-
def with_child
39-
synchronize do
40-
if alive?
41-
begin
42-
yield
43-
rescue Errno::ECONNRESET, Errno::EPIPE
44-
# The child has died but has not been collected by the wait thread yet,
45-
# so start a new child and try again.
46-
log "child dead; starting"
47-
start
48-
yield
49-
end
50-
else
51-
log "child not running; starting"
52-
start
53-
yield
54-
end
55-
end
56-
end
57-
58-
# Returns the pid of the process running the command, or nil if the application process died.
59-
def run(client)
60-
with_child do
61-
child.send_io client
62-
child.gets or raise Errno::EPIPE
63-
end
64-
65-
pid = child.gets.to_i
66-
67-
unless pid.zero?
68-
log "got worker pid #{pid}"
69-
pid
70-
end
71-
rescue Errno::ECONNRESET, Errno::EPIPE => e
72-
log "#{e} while reading from child; returning no pid"
73-
nil
74-
ensure
75-
client.close
76-
end
77-
78-
def stop
79-
log "stopping"
80-
@state = :stopping
81-
82-
if pid
83-
Process.kill('TERM', pid)
84-
Process.wait(pid)
85-
end
86-
rescue Errno::ESRCH, Errno::ECHILD
87-
# Don't care
88-
end
89-
90-
private
91-
92-
def start_child(preload = false)
93-
@child, child_socket = UNIXSocket.pair
94-
95-
Bundler.with_clean_env do
96-
@pid = Process.spawn(
97-
{
98-
"RAILS_ENV" => app_env,
99-
"RACK_ENV" => app_env,
100-
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
101-
"SPRING_PRELOAD" => preload ? "1" : "0"
102-
},
103-
"ruby",
104-
"-I", File.expand_path("../..", __FILE__),
105-
"-e", "require 'spring/application/boot'",
106-
3 => child_socket
107-
)
108-
end
109-
110-
start_wait_thread(pid, child) if child.gets
111-
child_socket.close
112-
end
113-
114-
def start_wait_thread(pid, child)
115-
Process.detach(pid)
116-
117-
Thread.new {
118-
# The recv can raise an ECONNRESET, killing the thread, but that's ok
119-
# as if it does we're no longer interested in the child
120-
loop do
121-
IO.select([child])
122-
break if child.recv(1, Socket::MSG_PEEK).empty?
123-
sleep 0.01
124-
end
125-
126-
log "child #{pid} shutdown"
127-
128-
synchronize {
129-
if @pid == pid
130-
@pid = nil
131-
restart
132-
end
133-
}
134-
}
135-
end
2+
module ApplicationManager
1363
end
1374
end
5+
6+
require 'spring/application_manager/fork_strategy'
7+
require 'spring/application_manager/pool_strategy'

0 commit comments

Comments
 (0)