Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 239ffb9

Browse files
committedNov 24, 2015
JRuby support: pooled application manager (instead of fork) - used if fork if not supported
1 parent 354637f commit 239ffb9

13 files changed

+521
-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+
child_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+
fork_child(client, streams, child_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 child_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 child_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 fork_child(client, streams, child_started)
37+
pid = fork { yield }
38+
child_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 fork_child(client, streams, child_started)
10+
child_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'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
module Spring
2+
module ApplicationManager
3+
class ForkStrategy
4+
attr_reader :pid, :child, :app_env, :spring_env, :status
5+
6+
def initialize(app_env)
7+
@app_env = app_env
8+
@spring_env = Env.new
9+
@mutex = Mutex.new
10+
@state = :running
11+
end
12+
13+
def log(message)
14+
spring_env.log "[application_manager:#{app_env}] #{message}"
15+
end
16+
17+
# We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
18+
# line which messes with backtraces in e.g. rspec
19+
def synchronize
20+
@mutex.lock
21+
yield
22+
ensure
23+
@mutex.unlock
24+
end
25+
26+
def start
27+
start_child
28+
end
29+
30+
def restart
31+
return if @state == :stopping
32+
start_child(true)
33+
end
34+
35+
def alive?
36+
@pid
37+
end
38+
39+
def with_child
40+
synchronize do
41+
if alive?
42+
begin
43+
yield
44+
rescue Errno::ECONNRESET, Errno::EPIPE
45+
# The child has died but has not been collected by the wait thread yet,
46+
# so start a new child and try again.
47+
log "child dead; starting"
48+
start
49+
yield
50+
end
51+
else
52+
log "child not running; starting"
53+
start
54+
yield
55+
end
56+
end
57+
end
58+
59+
# Returns the pid of the process running the command, or nil if the application process died.
60+
def run(client)
61+
with_child do
62+
child.send_io client
63+
child.gets or raise Errno::EPIPE
64+
end
65+
66+
pid = child.gets.to_i
67+
68+
unless pid.zero?
69+
log "got worker pid #{pid}"
70+
pid
71+
end
72+
rescue Errno::ECONNRESET, Errno::EPIPE => e
73+
log "#{e} while reading from child; returning no pid"
74+
nil
75+
ensure
76+
client.close
77+
end
78+
79+
def stop
80+
log "stopping"
81+
@state = :stopping
82+
83+
if pid
84+
Process.kill('TERM', pid)
85+
Process.wait(pid)
86+
end
87+
rescue Errno::ESRCH, Errno::ECHILD
88+
# Don't care
89+
end
90+
91+
private
92+
93+
def start_child(preload = false)
94+
@child, child_socket = UNIXSocket.pair
95+
96+
Bundler.with_clean_env do
97+
@pid = Process.spawn(
98+
{
99+
"RAILS_ENV" => app_env,
100+
"RACK_ENV" => app_env,
101+
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
102+
"SPRING_PRELOAD" => preload ? "1" : "0"
103+
},
104+
"ruby",
105+
"-I", File.expand_path("../..", __FILE__),
106+
"-e", "require 'spring/application/boot'",
107+
3 => child_socket
108+
)
109+
end
110+
111+
start_wait_thread(pid, child) if child.gets
112+
child_socket.close
113+
end
114+
115+
def start_wait_thread(pid, child)
116+
Process.detach(pid)
117+
118+
Thread.new {
119+
# The recv can raise an ECONNRESET, killing the thread, but that's ok
120+
# as if it does we're no longer interested in the child
121+
loop do
122+
IO.select([child])
123+
break if child.recv(1, Socket::MSG_PEEK).empty?
124+
sleep 0.01
125+
end
126+
127+
log "child #{pid} shutdown"
128+
129+
synchronize {
130+
if @pid == pid
131+
@pid = nil
132+
restart
133+
end
134+
}
135+
}
136+
end
137+
end
138+
end
139+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
require 'securerandom'
2+
3+
module Spring
4+
module ApplicationManager
5+
class PoolStrategy
6+
class Worker
7+
attr_reader :pid, :uuid, :socket
8+
attr_accessor :on_done
9+
10+
def initialize(env, args)
11+
@spring_env = Env.new
12+
@uuid = SecureRandom.uuid
13+
path = @spring_env.tmp_path.join("#{@uuid}.sock").to_s
14+
@server = UNIXServer.open(path)
15+
16+
Bundler.with_clean_env do
17+
spawn_app(
18+
env.merge("SPRING_SOCKET" => path),
19+
args
20+
)
21+
end
22+
23+
@socket = @server.accept
24+
end
25+
26+
def spawn_app(env, args)
27+
@pid =
28+
Process.spawn(
29+
env,
30+
*args
31+
)
32+
33+
log "(spawn #{@pid})"
34+
end
35+
36+
def await_boot
37+
@pid = socket.gets.to_i
38+
start_wait_thread(pid, socket) unless pid.zero?
39+
end
40+
41+
def start_wait_thread(pid, child)
42+
Process.detach(pid)
43+
44+
Thread.new {
45+
begin
46+
Process.kill(0, pid) while sleep(1)
47+
rescue Errno::ESRCH
48+
end
49+
50+
log "child #{pid} shutdown"
51+
52+
# socket.close
53+
# @server.close
54+
on_done.call(self) if on_done
55+
}
56+
end
57+
58+
def log(message)
59+
@spring_env.log "[worker:#{uuid}] #{message}"
60+
end
61+
end
62+
63+
class WorkerPool
64+
def initialize(app_env, *app_args)
65+
@app_env = app_env
66+
@app_args = app_args
67+
@spring_env = Env.new
68+
69+
@workers = []
70+
@workers_in_use = []
71+
@spawning_workers = []
72+
73+
@check_mutex = Mutex.new
74+
@workers_mutex = Mutex.new
75+
76+
run
77+
end
78+
79+
def add_worker
80+
worker = Worker.new(@app_env, @app_args)
81+
worker.on_done = method(:worker_done)
82+
@workers_mutex.synchronize { @spawning_workers << worker }
83+
Thread.new do
84+
worker.await_boot
85+
log "+ worker #{worker.pid} (#{worker.uuid})"
86+
@workers_mutex.synchronize do
87+
@spawning_workers.delete(worker)
88+
@workers << worker
89+
end
90+
end
91+
end
92+
93+
def worker_done(worker)
94+
log "- worker #{worker.pid} (#{worker.uuid})"
95+
@workers_mutex.synchronize do
96+
@workers_in_use.delete(worker)
97+
end
98+
end
99+
100+
def get_worker(spawn_new = true)
101+
add_worker if spawn_new && all_size == 0
102+
103+
worker = nil
104+
while worker.nil? && all_size > 0
105+
@workers_mutex.synchronize do
106+
worker = @workers.shift
107+
@workers_in_use << worker if worker
108+
end
109+
break if worker
110+
sleep 1
111+
end
112+
113+
Thread.new { check_min_free_workers } if spawn_new
114+
115+
worker
116+
end
117+
118+
def check_min_free_workers
119+
if @check_mutex.try_lock
120+
while all_size < Spring.pool_min_free_workers
121+
unless Spring.pool_spawn_parallel
122+
sleep 0.1 until @workers_mutex.synchronize { @spawning_workers.empty? }
123+
end
124+
add_worker
125+
end
126+
@check_mutex.unlock
127+
end
128+
end
129+
130+
def all_size
131+
@workers_mutex.synchronize { @workers.size + @spawning_workers.size }
132+
end
133+
134+
def stop!
135+
if spawning_worker_pids.include?(nil)
136+
log "Waiting for workers to quit..."
137+
sleep 0.1 while spawning_worker_pids.include?(nil)
138+
end
139+
140+
@workers_mutex.synchronize do
141+
(@spawning_workers + @workers_in_use + @workers).each do |worker|
142+
kill_worker(worker)
143+
end
144+
end
145+
end
146+
private
147+
def kill_worker(worker)
148+
log "- worker #{worker.pid} (#{worker.uuid})."
149+
system("kill -9 #{worker.pid} > /dev/null 2>&1")
150+
rescue
151+
end
152+
153+
def spawning_worker_pids
154+
@spawning_workers.map { |worker| worker.pid }
155+
end
156+
157+
def run
158+
check_min_free_workers
159+
end
160+
161+
def log(message)
162+
@spring_env.log "[worker:pool] #{message}"
163+
end
164+
end
165+
166+
def initialize(app_env)
167+
@app_env = app_env
168+
@spring_env = Env.new
169+
@pool =
170+
WorkerPool.new(
171+
{
172+
"RAILS_ENV" => app_env,
173+
"RACK_ENV" => app_env,
174+
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
175+
"SPRING_PRELOAD" => "1",
176+
},
177+
Spring.ruby_bin,
178+
"-I", File.expand_path("../..", __FILE__),
179+
"-e", "require 'spring/application/boot'"
180+
)
181+
end
182+
183+
# Returns the name of the screen running the command, or nil if the application process died.
184+
def run(client)
185+
pid = nil
186+
with_child do |child|
187+
child.socket.send_io(client)
188+
IO.select([child.socket])
189+
child.socket.gets or raise Errno::EPIPE
190+
IO.select([child.socket])
191+
pid = child.socket.gets.to_i
192+
end
193+
194+
unless pid.zero?
195+
log "got worker pid #{pid}"
196+
pid
197+
end
198+
rescue Errno::ECONNRESET, Errno::EPIPE => e
199+
log "#{e} while reading from child; returning no pid"
200+
nil
201+
ensure
202+
client.close
203+
end
204+
205+
def stop
206+
log "stopping"
207+
208+
@pool.stop!
209+
rescue Errno::ESRCH, Errno::ECHILD
210+
# Don't care
211+
end
212+
213+
protected
214+
215+
attr_reader :app_env, :spring_env
216+
217+
def log(message)
218+
spring_env.log "[application_manager:#{app_env}] #{message}"
219+
end
220+
221+
def with_child
222+
yield(@pool.get_worker)
223+
end
224+
end
225+
end
226+
end

‎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/client/run.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
require "rbconfig"
22
require "socket"
33
require "bundler"
4+
require "spring/platform"
45

56
module Spring
67
module Client
78
class Run < Command
8-
FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO) & Signal.list.keys
9-
TIMEOUT = 1
9+
TIMEOUT = Spring.fork? ? 1 : 60
1010

1111
def initialize(args)
1212
super
@@ -127,6 +127,7 @@ def run_command(client, 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

@@ -139,6 +140,7 @@ def run_command(client, application)
139140
log "got pid: #{pid}"
140141

141142
forward_signals(pid.to_i)
143+
IO.select([application])
142144
status = application.read.to_i
143145

144146
log "got exit status #{status}"

‎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/platform.rb

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module Spring
2+
def self.fork?
3+
Process.respond_to?(:fork)
4+
end
5+
6+
def self.jruby?
7+
RUBY_PLATFORM == "java"
8+
end
9+
10+
def self.ruby_bin
11+
if RUBY_PLATFORM == "java"
12+
"jruby"
13+
else
14+
"ruby"
15+
end
16+
end
17+
18+
if jruby?
19+
IGNORE_SIGNALS = %w(INT)
20+
FORWARDED_SIGNALS = %w(INT USR2 INFO) & Signal.list.keys
21+
else
22+
IGNORE_SIGNALS = %w(INT QUIT)
23+
FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO) & Signal.list.keys
24+
end
25+
end

‎lib/spring/server.rb

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Spring
33
end
44

55
require "spring/boot"
6+
require "spring/platform"
67
require "spring/application_manager"
78

89
# Must be last, as it requires bundler/setup, which alters the load path
@@ -18,7 +19,7 @@ def self.boot
1819

1920
def initialize(env = Env.new)
2021
@env = env
21-
@applications = Hash.new { |h, k| h[k] = ApplicationManager.new(k) }
22+
@applications = Hash.new { |h, k| h[k] = new_application_manager(k) }
2223
@pidfile = env.pidfile_path.open('a')
2324
@mutex = Mutex.new
2425
end
@@ -126,5 +127,13 @@ def set_process_title
126127
"spring server | #{env.app_name} | started #{distance} ago"
127128
}
128129
end
130+
131+
def new_application_manager(app_env)
132+
if Spring.fork?
133+
ApplicationManager::ForkStrategy.new(app_env)
134+
else
135+
ApplicationManager::PoolStrategy.new(app_env)
136+
end
137+
end
129138
end
130139
end

0 commit comments

Comments
 (0)
Please sign in to comment.