puma 3.11.4 → 6.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +1717 -432
- data/LICENSE +23 -20
- data/README.md +190 -64
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +59 -21
- data/docs/compile_options.md +55 -0
- data/docs/deployment.md +69 -58
- data/docs/fork_worker.md +31 -0
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/jungle/README.md +9 -0
- data/{tools → docs}/jungle/rc.d/README.md +1 -1
- data/{tools → docs}/jungle/rc.d/puma +2 -2
- data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +2 -2
- data/docs/plugins.md +22 -12
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +47 -22
- data/docs/signals.md +13 -11
- data/docs/stats.md +142 -0
- data/docs/systemd.md +95 -120
- data/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -2
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +61 -3
- data/ext/puma_http11/http11_parser.c +106 -118
- data/ext/puma_http11/http11_parser.h +2 -2
- data/ext/puma_http11/http11_parser.java.rl +22 -38
- data/ext/puma_http11/http11_parser.rl +6 -4
- data/ext/puma_http11/http11_parser_common.rl +6 -6
- data/ext/puma_http11/mini_ssl.c +376 -93
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +250 -88
- data/ext/puma_http11/puma_http11.c +49 -57
- data/lib/puma/app/status.rb +71 -49
- data/lib/puma/binder.rb +243 -148
- data/lib/puma/cli.rb +50 -36
- data/lib/puma/client.rb +373 -233
- data/lib/puma/cluster/worker.rb +175 -0
- data/lib/puma/cluster/worker_handle.rb +97 -0
- data/lib/puma/cluster.rb +268 -235
- data/lib/puma/commonlogger.rb +4 -2
- data/lib/puma/configuration.rb +116 -88
- data/lib/puma/const.rb +49 -30
- data/lib/puma/control_cli.rb +123 -76
- data/lib/puma/detect.rb +33 -2
- data/lib/puma/dsl.rb +685 -135
- data/lib/puma/error_logger.rb +112 -0
- data/lib/puma/events.rb +17 -111
- data/lib/puma/io_buffer.rb +44 -5
- data/lib/puma/jruby_restart.rb +4 -59
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +196 -130
- data/lib/puma/log_writer.rb +137 -0
- data/lib/puma/minissl/context_builder.rb +92 -0
- data/lib/puma/minissl.rb +249 -69
- data/lib/puma/null_io.rb +20 -1
- data/lib/puma/plugin/tmp_restart.rb +3 -1
- data/lib/puma/plugin.rb +9 -13
- data/lib/puma/rack/builder.rb +8 -9
- data/lib/puma/rack/urlmap.rb +2 -0
- data/lib/puma/rack_default.rb +3 -1
- data/lib/puma/reactor.rb +90 -187
- data/lib/puma/request.rb +644 -0
- data/lib/puma/runner.rb +94 -71
- data/lib/puma/server.rb +337 -715
- data/lib/puma/single.rb +27 -72
- data/lib/puma/state_file.rb +46 -7
- data/lib/puma/systemd.rb +47 -0
- data/lib/puma/thread_pool.rb +184 -93
- data/lib/puma/util.rb +23 -10
- data/lib/puma.rb +60 -3
- data/lib/rack/handler/puma.rb +17 -15
- data/tools/Dockerfile +16 -0
- data/tools/trickletest.rb +0 -1
- metadata +53 -33
- data/ext/puma_http11/io_buffer.c +0 -155
- data/lib/puma/accept_nonblock.rb +0 -23
- data/lib/puma/compat.rb +0 -14
- data/lib/puma/convenient.rb +0 -23
- data/lib/puma/daemon_ext.rb +0 -31
- data/lib/puma/delegation.rb +0 -11
- data/lib/puma/java_io_buffer.rb +0 -45
- data/lib/puma/rack/backports/uri/common_193.rb +0 -33
- data/lib/puma/tcp_logger.rb +0 -39
- data/tools/jungle/README.md +0 -19
- data/tools/jungle/init.d/README.md +0 -61
- data/tools/jungle/init.d/puma +0 -421
- data/tools/jungle/init.d/run-puma +0 -18
- data/tools/jungle/upstart/README.md +0 -61
- data/tools/jungle/upstart/puma-manager.conf +0 -31
- data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/cluster.rb
CHANGED
@@ -1,36 +1,53 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'runner'
|
4
|
+
require_relative 'util'
|
5
|
+
require_relative 'plugin'
|
6
|
+
require_relative 'cluster/worker_handle'
|
7
|
+
require_relative 'cluster/worker'
|
4
8
|
|
5
9
|
require 'time'
|
6
10
|
|
7
11
|
module Puma
|
12
|
+
# This class is instantiated by the `Puma::Launcher` and used
|
13
|
+
# to boot and serve a Ruby application when puma "workers" are needed
|
14
|
+
# i.e. when using multi-processes. For example `$ puma -w 5`
|
15
|
+
#
|
16
|
+
# An instance of this class will spawn the number of processes passed in
|
17
|
+
# via the `spawn_workers` method call. Each worker will have it's own
|
18
|
+
# instance of a `Puma::Server`.
|
8
19
|
class Cluster < Runner
|
9
|
-
|
10
|
-
|
11
|
-
def initialize(cli, events)
|
12
|
-
super cli, events
|
20
|
+
def initialize(launcher)
|
21
|
+
super(launcher)
|
13
22
|
|
14
23
|
@phase = 0
|
15
24
|
@workers = []
|
16
|
-
@next_check =
|
25
|
+
@next_check = Time.now
|
17
26
|
|
18
|
-
@phased_state = :idle
|
19
27
|
@phased_restart = false
|
20
28
|
end
|
21
29
|
|
30
|
+
# Returns the list of cluster worker handles.
|
31
|
+
# @return [Array<Puma::Cluster::WorkerHandle>]
|
32
|
+
attr_reader :workers
|
33
|
+
|
22
34
|
def stop_workers
|
23
35
|
log "- Gracefully shutting down workers..."
|
24
36
|
@workers.each { |x| x.term }
|
25
37
|
|
26
38
|
begin
|
27
|
-
|
39
|
+
loop do
|
40
|
+
wait_workers
|
41
|
+
break if @workers.reject {|w| w.pid.nil?}.empty?
|
42
|
+
sleep 0.2
|
43
|
+
end
|
28
44
|
rescue Interrupt
|
29
45
|
log "! Cancelled waiting for workers"
|
30
46
|
end
|
31
47
|
end
|
32
48
|
|
33
49
|
def start_phased_restart
|
50
|
+
@events.fire_on_restart!
|
34
51
|
@phase += 1
|
35
52
|
log "- Starting phased worker restart, phase: #{@phase}"
|
36
53
|
|
@@ -47,155 +64,103 @@ module Puma
|
|
47
64
|
@workers.each { |x| x.hup }
|
48
65
|
end
|
49
66
|
|
50
|
-
class Worker
|
51
|
-
def initialize(idx, pid, phase, options)
|
52
|
-
@index = idx
|
53
|
-
@pid = pid
|
54
|
-
@phase = phase
|
55
|
-
@stage = :started
|
56
|
-
@signal = "TERM"
|
57
|
-
@options = options
|
58
|
-
@first_term_sent = nil
|
59
|
-
@last_checkin = Time.now
|
60
|
-
@last_status = '{}'
|
61
|
-
@dead = false
|
62
|
-
end
|
63
|
-
|
64
|
-
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status
|
65
|
-
|
66
|
-
def booted?
|
67
|
-
@stage == :booted
|
68
|
-
end
|
69
|
-
|
70
|
-
def boot!
|
71
|
-
@last_checkin = Time.now
|
72
|
-
@stage = :booted
|
73
|
-
end
|
74
|
-
|
75
|
-
def dead?
|
76
|
-
@dead
|
77
|
-
end
|
78
|
-
|
79
|
-
def dead!
|
80
|
-
@dead = true
|
81
|
-
end
|
82
|
-
|
83
|
-
def ping!(status)
|
84
|
-
@last_checkin = Time.now
|
85
|
-
@last_status = status
|
86
|
-
end
|
87
|
-
|
88
|
-
def ping_timeout?(which)
|
89
|
-
Time.now - @last_checkin > which
|
90
|
-
end
|
91
|
-
|
92
|
-
def term
|
93
|
-
begin
|
94
|
-
if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
|
95
|
-
@signal = "KILL"
|
96
|
-
else
|
97
|
-
@first_term_sent ||= Time.now
|
98
|
-
end
|
99
|
-
|
100
|
-
Process.kill @signal, @pid
|
101
|
-
rescue Errno::ESRCH
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def kill
|
106
|
-
Process.kill "KILL", @pid
|
107
|
-
rescue Errno::ESRCH
|
108
|
-
end
|
109
|
-
|
110
|
-
def hup
|
111
|
-
Process.kill "HUP", @pid
|
112
|
-
rescue Errno::ESRCH
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
67
|
def spawn_workers
|
117
68
|
diff = @options[:workers] - @workers.size
|
118
69
|
return if diff < 1
|
119
70
|
|
120
71
|
master = Process.pid
|
72
|
+
if @options[:fork_worker]
|
73
|
+
@fork_writer << "-1\n"
|
74
|
+
end
|
121
75
|
|
122
76
|
diff.times do
|
123
77
|
idx = next_worker_index
|
124
|
-
@launcher.config.run_hooks :before_worker_fork, idx
|
125
78
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
79
|
+
if @options[:fork_worker] && idx != 0
|
80
|
+
@fork_writer << "#{idx}\n"
|
81
|
+
pid = nil
|
82
|
+
else
|
83
|
+
pid = spawn_worker(idx, master)
|
131
84
|
end
|
132
85
|
|
133
86
|
debug "Spawned worker: #{pid}"
|
134
|
-
@workers <<
|
87
|
+
@workers << WorkerHandle.new(idx, pid, @phase, @options)
|
88
|
+
end
|
89
|
+
|
90
|
+
if @options[:fork_worker] &&
|
91
|
+
@workers.all? {|x| x.phase == @phase}
|
135
92
|
|
136
|
-
@
|
93
|
+
@fork_writer << "0\n"
|
137
94
|
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# @version 5.0.0
|
98
|
+
def spawn_worker(idx, master)
|
99
|
+
@config.run_hooks(:before_worker_fork, idx, @log_writer)
|
138
100
|
|
139
|
-
|
140
|
-
|
101
|
+
pid = fork { worker(idx, master) }
|
102
|
+
if !pid
|
103
|
+
log "! Complete inability to spawn new workers detected"
|
104
|
+
log "! Seppuku is the only choice."
|
105
|
+
exit! 1
|
141
106
|
end
|
107
|
+
|
108
|
+
@config.run_hooks(:after_worker_fork, idx, @log_writer)
|
109
|
+
pid
|
142
110
|
end
|
143
111
|
|
144
112
|
def cull_workers
|
145
113
|
diff = @workers.size - @options[:workers]
|
146
114
|
return if diff < 1
|
115
|
+
debug "Culling #{diff} workers"
|
147
116
|
|
148
|
-
|
117
|
+
workers = workers_to_cull(diff)
|
118
|
+
debug "Workers to cull: #{workers.inspect}"
|
149
119
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
workers_to_cull.each do |worker|
|
154
|
-
log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
|
120
|
+
workers.each do |worker|
|
121
|
+
log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
|
155
122
|
worker.term
|
156
123
|
end
|
157
124
|
end
|
158
125
|
|
159
|
-
def
|
160
|
-
|
161
|
-
occupied_positions = @workers.map { |w| w.index }
|
162
|
-
available_positions = all_positions.to_a - occupied_positions
|
163
|
-
available_positions.first
|
164
|
-
end
|
165
|
-
|
166
|
-
def all_workers_booted?
|
167
|
-
@workers.count { |w| !w.booted? } == 0
|
168
|
-
end
|
169
|
-
|
170
|
-
def check_workers(force=false)
|
171
|
-
return if !force && @next_check && @next_check >= Time.now
|
126
|
+
def workers_to_cull(diff)
|
127
|
+
workers = @workers.sort_by(&:started_at)
|
172
128
|
|
173
|
-
|
129
|
+
# In fork_worker mode, worker 0 acts as our master process.
|
130
|
+
# We should avoid culling it to preserve copy-on-write memory gains.
|
131
|
+
workers.reject! { |w| w.index == 0 } if @options[:fork_worker]
|
174
132
|
|
175
|
-
|
133
|
+
workers[cull_start_index(diff), diff]
|
134
|
+
end
|
176
135
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
end
|
136
|
+
def cull_start_index(diff)
|
137
|
+
case @options[:worker_culling_strategy]
|
138
|
+
when :oldest
|
139
|
+
0
|
140
|
+
else # :youngest
|
141
|
+
-diff
|
184
142
|
end
|
143
|
+
end
|
185
144
|
|
186
|
-
|
187
|
-
|
188
|
-
|
145
|
+
# @!attribute [r] next_worker_index
|
146
|
+
def next_worker_index
|
147
|
+
occupied_positions = @workers.map(&:index)
|
148
|
+
idx = 0
|
149
|
+
idx += 1 until !occupied_positions.include?(idx)
|
150
|
+
idx
|
151
|
+
end
|
189
152
|
|
190
|
-
|
191
|
-
|
192
|
-
|
153
|
+
def all_workers_booted?
|
154
|
+
@workers.count { |w| !w.booted? } == 0
|
155
|
+
end
|
193
156
|
|
194
|
-
|
195
|
-
|
157
|
+
def check_workers
|
158
|
+
return if @next_check >= Time.now
|
196
159
|
|
197
|
-
@
|
160
|
+
@next_check = Time.now + @options[:worker_check_interval]
|
198
161
|
|
162
|
+
timeout_workers
|
163
|
+
wait_workers
|
199
164
|
cull_workers
|
200
165
|
spawn_workers
|
201
166
|
|
@@ -207,97 +172,40 @@ module Puma
|
|
207
172
|
w = @workers.find { |x| x.phase != @phase }
|
208
173
|
|
209
174
|
if w
|
210
|
-
|
211
|
-
|
212
|
-
|
175
|
+
log "- Stopping #{w.pid} for phased upgrade..."
|
176
|
+
unless w.term?
|
177
|
+
w.term
|
178
|
+
log "- #{w.signal} sent to #{w.pid}..."
|
213
179
|
end
|
214
|
-
|
215
|
-
w.term
|
216
|
-
log "- #{w.signal} sent to #{w.pid}..."
|
217
180
|
end
|
218
181
|
end
|
219
|
-
end
|
220
182
|
|
221
|
-
|
222
|
-
|
183
|
+
t = @workers.reject(&:term?)
|
184
|
+
t.map!(&:ping_timeout)
|
223
185
|
|
224
|
-
|
225
|
-
@wakeup.write "!" unless @wakeup.closed?
|
226
|
-
rescue SystemCallError, IOError
|
227
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
228
|
-
end
|
186
|
+
@next_check = [t.min, @next_check].compact.min
|
229
187
|
end
|
230
188
|
|
231
189
|
def worker(index, master)
|
232
|
-
title = "puma: cluster worker #{index}: #{master}"
|
233
|
-
title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
|
234
|
-
$0 = title
|
235
|
-
|
236
|
-
Signal.trap "SIGINT", "IGNORE"
|
237
|
-
|
238
190
|
@workers = []
|
191
|
+
|
239
192
|
@master_read.close
|
240
193
|
@suicide_pipe.close
|
194
|
+
@fork_writer.close
|
241
195
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
end
|
247
|
-
|
248
|
-
# If we're not running under a Bundler context, then
|
249
|
-
# report the info about the context we will be using
|
250
|
-
if !ENV['BUNDLE_GEMFILE']
|
251
|
-
if File.exist?("Gemfile")
|
252
|
-
log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
|
253
|
-
elsif File.exist?("gems.rb")
|
254
|
-
log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
# Invoke any worker boot hooks so they can get
|
259
|
-
# things in shape before booting the app.
|
260
|
-
@launcher.config.run_hooks :before_worker_boot, index
|
261
|
-
|
262
|
-
server = start_server
|
263
|
-
|
264
|
-
Signal.trap "SIGTERM" do
|
265
|
-
server.stop
|
266
|
-
end
|
267
|
-
|
268
|
-
begin
|
269
|
-
@worker_write << "b#{Process.pid}\n"
|
270
|
-
rescue SystemCallError, IOError
|
271
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
272
|
-
STDERR.puts "Master seems to have exited, exiting."
|
273
|
-
return
|
274
|
-
end
|
275
|
-
|
276
|
-
Thread.new(@worker_write) do |io|
|
277
|
-
base_payload = "p#{Process.pid}"
|
278
|
-
|
279
|
-
while true
|
280
|
-
sleep WORKER_CHECK_INTERVAL
|
281
|
-
begin
|
282
|
-
b = server.backlog || 0
|
283
|
-
r = server.running || 0
|
284
|
-
payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
|
285
|
-
io << payload
|
286
|
-
rescue IOError
|
287
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
288
|
-
break
|
289
|
-
end
|
290
|
-
end
|
196
|
+
pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
|
197
|
+
if @options[:fork_worker]
|
198
|
+
pipes[:fork_pipe] = @fork_pipe
|
199
|
+
pipes[:wakeup] = @wakeup
|
291
200
|
end
|
292
201
|
|
293
|
-
server
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
@worker_write.close
|
202
|
+
server = start_server if preload?
|
203
|
+
new_worker = Worker.new index: index,
|
204
|
+
master: master,
|
205
|
+
launcher: @launcher,
|
206
|
+
pipes: pipes,
|
207
|
+
server: server
|
208
|
+
new_worker.run
|
301
209
|
end
|
302
210
|
|
303
211
|
def restart
|
@@ -305,8 +213,8 @@ module Puma
|
|
305
213
|
stop
|
306
214
|
end
|
307
215
|
|
308
|
-
def phased_restart
|
309
|
-
return false if @options[:preload_app]
|
216
|
+
def phased_restart(refork = false)
|
217
|
+
return false if @options[:preload_app] && !refork
|
310
218
|
|
311
219
|
@phased_restart = true
|
312
220
|
wakeup!
|
@@ -322,7 +230,7 @@ module Puma
|
|
322
230
|
def stop_blocked
|
323
231
|
@status = :stop if @status == :run
|
324
232
|
wakeup!
|
325
|
-
@control
|
233
|
+
@control&.stop true
|
326
234
|
Process.waitall
|
327
235
|
end
|
328
236
|
|
@@ -337,20 +245,63 @@ module Puma
|
|
337
245
|
Dir.chdir dir
|
338
246
|
end
|
339
247
|
|
248
|
+
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
249
|
+
# the master process.
|
250
|
+
# @!attribute [r] stats
|
340
251
|
def stats
|
341
252
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
342
|
-
|
343
|
-
|
344
|
-
|
253
|
+
worker_status = @workers.map do |w|
|
254
|
+
{
|
255
|
+
started_at: w.started_at.utc.iso8601,
|
256
|
+
pid: w.pid,
|
257
|
+
index: w.index,
|
258
|
+
phase: w.phase,
|
259
|
+
booted: w.booted?,
|
260
|
+
last_checkin: w.last_checkin.utc.iso8601,
|
261
|
+
last_status: w.last_status,
|
262
|
+
}
|
263
|
+
end
|
264
|
+
|
265
|
+
{
|
266
|
+
started_at: @started_at.utc.iso8601,
|
267
|
+
workers: @workers.size,
|
268
|
+
phase: @phase,
|
269
|
+
booted_workers: worker_status.count { |w| w[:booted] },
|
270
|
+
old_workers: old_worker_count,
|
271
|
+
worker_status: worker_status,
|
272
|
+
}.merge(super)
|
345
273
|
end
|
346
274
|
|
347
275
|
def preload?
|
348
276
|
@options[:preload_app]
|
349
277
|
end
|
350
278
|
|
279
|
+
# @version 5.0.0
|
280
|
+
def fork_worker!
|
281
|
+
if (worker = @workers.find { |w| w.index == 0 })
|
282
|
+
worker.phase += 1
|
283
|
+
end
|
284
|
+
phased_restart(true)
|
285
|
+
end
|
286
|
+
|
351
287
|
# We do this in a separate method to keep the lambda scope
|
352
288
|
# of the signals handlers as small as possible.
|
353
289
|
def setup_signals
|
290
|
+
if @options[:fork_worker]
|
291
|
+
Signal.trap "SIGURG" do
|
292
|
+
fork_worker!
|
293
|
+
end
|
294
|
+
|
295
|
+
# Auto-fork after the specified number of requests.
|
296
|
+
if (fork_requests = @options[:fork_worker].to_i) > 0
|
297
|
+
@events.register(:ping!) do |w|
|
298
|
+
fork_worker! if w.index == 0 &&
|
299
|
+
w.phase == 0 &&
|
300
|
+
w.last_status[:requests_count] >= fork_requests
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
354
305
|
Signal.trap "SIGCHLD" do
|
355
306
|
wakeup!
|
356
307
|
end
|
@@ -375,10 +326,13 @@ module Puma
|
|
375
326
|
log "Early termination of worker"
|
376
327
|
exit! 0
|
377
328
|
else
|
329
|
+
@launcher.close_binder_listeners
|
330
|
+
|
378
331
|
stop_workers
|
379
332
|
stop
|
380
|
-
|
381
|
-
raise
|
333
|
+
@events.fire_on_stopped!
|
334
|
+
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
335
|
+
exit 0 # Clean exit, workers were stopped
|
382
336
|
end
|
383
337
|
end
|
384
338
|
end
|
@@ -388,15 +342,25 @@ module Puma
|
|
388
342
|
|
389
343
|
output_header "cluster"
|
390
344
|
|
391
|
-
|
392
|
-
|
393
|
-
before = Thread.list
|
345
|
+
# This is aligned with the output from Runner, see Runner#output_header
|
346
|
+
log "* Workers: #{@options[:workers]}"
|
394
347
|
|
395
348
|
if preload?
|
349
|
+
# Threads explicitly marked as fork safe will be ignored. Used in Rails,
|
350
|
+
# but may be used by anyone. Note that we need to explicit
|
351
|
+
# Process::Waiter check here because there's a bug in Ruby 2.6 and below
|
352
|
+
# where calling thread_variable_get on a Process::Waiter will segfault.
|
353
|
+
# We can drop that clause once those versions of Ruby are no longer
|
354
|
+
# supported.
|
355
|
+
fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
|
356
|
+
|
357
|
+
before = Thread.list.reject(&fork_safe)
|
358
|
+
|
359
|
+
log "* Restarts: (\u2714) hot (\u2716) phased"
|
396
360
|
log "* Preloading application"
|
397
361
|
load_and_bind
|
398
362
|
|
399
|
-
after = Thread.list
|
363
|
+
after = Thread.list.reject(&fork_safe)
|
400
364
|
|
401
365
|
if after.size > before.size
|
402
366
|
threads = (after - before)
|
@@ -410,14 +374,14 @@ module Puma
|
|
410
374
|
end
|
411
375
|
end
|
412
376
|
else
|
413
|
-
log "*
|
377
|
+
log "* Restarts: (\u2714) hot (\u2714) phased"
|
414
378
|
|
415
|
-
unless @
|
379
|
+
unless @config.app_configured?
|
416
380
|
error "No application configured, nothing to run"
|
417
381
|
exit 1
|
418
382
|
end
|
419
383
|
|
420
|
-
@launcher.binder.parse @options[:binds]
|
384
|
+
@launcher.binder.parse @options[:binds]
|
421
385
|
end
|
422
386
|
|
423
387
|
read, @wakeup = Puma::Util.pipe
|
@@ -431,12 +395,13 @@ module Puma
|
|
431
395
|
#
|
432
396
|
@check_pipe, @suicide_pipe = Puma::Util.pipe
|
433
397
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
398
|
+
# Separate pipe used by worker 0 to receive commands to
|
399
|
+
# fork new worker processes.
|
400
|
+
@fork_pipe, @fork_writer = Puma::Util.pipe
|
401
|
+
|
402
|
+
log "Use Ctrl-C to stop"
|
403
|
+
|
404
|
+
single_worker_warning
|
440
405
|
|
441
406
|
redirect_io
|
442
407
|
|
@@ -448,7 +413,7 @@ module Puma
|
|
448
413
|
|
449
414
|
@master_read, @worker_write = read, @wakeup
|
450
415
|
|
451
|
-
@
|
416
|
+
@config.run_hooks(:before_fork, nil, @log_writer)
|
452
417
|
|
453
418
|
spawn_workers
|
454
419
|
|
@@ -456,48 +421,65 @@ module Puma
|
|
456
421
|
stop
|
457
422
|
end
|
458
423
|
|
459
|
-
@launcher.events.fire_on_booted!
|
460
|
-
|
461
424
|
begin
|
462
|
-
|
425
|
+
booted = false
|
426
|
+
in_phased_restart = false
|
427
|
+
workers_not_booted = @options[:workers]
|
463
428
|
|
464
429
|
while @status == :run
|
465
430
|
begin
|
466
431
|
if @phased_restart
|
467
432
|
start_phased_restart
|
468
433
|
@phased_restart = false
|
434
|
+
in_phased_restart = true
|
435
|
+
workers_not_booted = @options[:workers]
|
469
436
|
end
|
470
437
|
|
471
|
-
check_workers
|
438
|
+
check_workers
|
472
439
|
|
473
|
-
|
474
|
-
|
475
|
-
res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
|
476
|
-
|
477
|
-
if res
|
440
|
+
if read.wait_readable([0, @next_check - Time.now].max)
|
478
441
|
req = read.read_nonblock(1)
|
479
442
|
|
443
|
+
@next_check = Time.now if req == "!"
|
480
444
|
next if !req || req == "!"
|
481
445
|
|
482
446
|
result = read.gets
|
483
447
|
pid = result.to_i
|
484
448
|
|
449
|
+
if req == "b" || req == "f"
|
450
|
+
pid, idx = result.split(':').map(&:to_i)
|
451
|
+
w = @workers.find {|x| x.index == idx}
|
452
|
+
w.pid = pid if w.pid.nil?
|
453
|
+
end
|
454
|
+
|
485
455
|
if w = @workers.find { |x| x.pid == pid }
|
486
456
|
case req
|
487
457
|
when "b"
|
488
458
|
w.boot!
|
489
|
-
log "- Worker #{w.index} (
|
490
|
-
|
459
|
+
log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
|
460
|
+
@next_check = Time.now
|
461
|
+
workers_not_booted -= 1
|
462
|
+
when "e"
|
463
|
+
# external term, see worker method, Signal.trap "SIGTERM"
|
464
|
+
w.term!
|
491
465
|
when "t"
|
492
|
-
w.
|
493
|
-
force_check = true
|
466
|
+
w.term unless w.term?
|
494
467
|
when "p"
|
495
468
|
w.ping!(result.sub(/^\d+/,'').chomp)
|
469
|
+
@events.fire(:ping!, w)
|
470
|
+
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
471
|
+
@events.fire_on_booted!
|
472
|
+
booted = true
|
473
|
+
end
|
496
474
|
end
|
497
475
|
else
|
498
476
|
log "! Out-of-sync worker list, no #{pid} worker"
|
499
477
|
end
|
500
478
|
end
|
479
|
+
if in_phased_restart && workers_not_booted.zero?
|
480
|
+
@events.fire_on_booted!
|
481
|
+
in_phased_restart = false
|
482
|
+
end
|
501
483
|
|
502
484
|
rescue Interrupt
|
503
485
|
@status = :stop
|
@@ -512,5 +494,56 @@ module Puma
|
|
512
494
|
@wakeup.close
|
513
495
|
end
|
514
496
|
end
|
497
|
+
|
498
|
+
private
|
499
|
+
|
500
|
+
def single_worker_warning
|
501
|
+
return if @options[:workers] != 1 || @options[:silence_single_worker_warning]
|
502
|
+
|
503
|
+
log "! WARNING: Detected running cluster mode with 1 worker."
|
504
|
+
log "! Running Puma in cluster mode with a single worker is often a misconfiguration."
|
505
|
+
log "! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead."
|
506
|
+
log "! Set the `silence_single_worker_warning` option to silence this warning message."
|
507
|
+
end
|
508
|
+
|
509
|
+
# loops thru @workers, removing workers that exited, and calling
|
510
|
+
# `#term` if needed
|
511
|
+
def wait_workers
|
512
|
+
@workers.reject! do |w|
|
513
|
+
next false if w.pid.nil?
|
514
|
+
begin
|
515
|
+
if Process.wait(w.pid, Process::WNOHANG)
|
516
|
+
true
|
517
|
+
else
|
518
|
+
w.term if w.term?
|
519
|
+
nil
|
520
|
+
end
|
521
|
+
rescue Errno::ECHILD
|
522
|
+
begin
|
523
|
+
Process.kill(0, w.pid)
|
524
|
+
# child still alive but has another parent (e.g., using fork_worker)
|
525
|
+
w.term if w.term?
|
526
|
+
false
|
527
|
+
rescue Errno::ESRCH, Errno::EPERM
|
528
|
+
true # child is already terminated
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# @version 5.0.0
|
535
|
+
def timeout_workers
|
536
|
+
@workers.each do |w|
|
537
|
+
if !w.term? && w.ping_timeout <= Time.now
|
538
|
+
details = if w.booted?
|
539
|
+
"(worker failed to check in within #{@options[:worker_timeout]} seconds)"
|
540
|
+
else
|
541
|
+
"(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
|
542
|
+
end
|
543
|
+
log "! Terminating timed out worker #{details}: #{w.pid}"
|
544
|
+
w.kill
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
515
548
|
end
|
516
549
|
end
|