puma 4.3.12 → 6.0.0
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 +1591 -521
- data/LICENSE +23 -20
- data/README.md +130 -42
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +63 -26
- data/docs/compile_options.md +55 -0
- data/docs/deployment.md +60 -69
- data/docs/fork_worker.md +31 -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 +1 -1
- data/docs/plugins.md +15 -15
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +46 -23
- data/docs/signals.md +13 -11
- data/docs/stats.md +142 -0
- data/docs/systemd.md +85 -128
- 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 -4
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +49 -12
- data/ext/puma_http11/http11_parser.c +46 -48
- data/ext/puma_http11/http11_parser.h +2 -2
- data/ext/puma_http11/http11_parser.java.rl +3 -3
- data/ext/puma_http11/http11_parser.rl +3 -3
- data/ext/puma_http11/http11_parser_common.rl +2 -2
- data/ext/puma_http11/mini_ssl.c +250 -93
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +4 -6
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
- data/ext/puma_http11/puma_http11.c +46 -57
- data/lib/puma/app/status.rb +52 -38
- data/lib/puma/binder.rb +232 -119
- data/lib/puma/cli.rb +33 -33
- data/lib/puma/client.rb +125 -87
- data/lib/puma/cluster/worker.rb +175 -0
- data/lib/puma/cluster/worker_handle.rb +97 -0
- data/lib/puma/cluster.rb +224 -229
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +112 -87
- data/lib/puma/const.rb +25 -22
- data/lib/puma/control_cli.rb +99 -79
- data/lib/puma/detect.rb +31 -2
- data/lib/puma/dsl.rb +423 -110
- data/lib/puma/error_logger.rb +112 -0
- data/lib/puma/events.rb +16 -115
- data/lib/puma/io_buffer.rb +34 -2
- data/lib/puma/jruby_restart.rb +2 -59
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +170 -148
- data/lib/puma/log_writer.rb +137 -0
- data/lib/puma/minissl/context_builder.rb +35 -19
- data/lib/puma/minissl.rb +213 -55
- data/lib/puma/null_io.rb +18 -1
- data/lib/puma/plugin/tmp_restart.rb +1 -1
- data/lib/puma/plugin.rb +3 -12
- data/lib/puma/rack/builder.rb +5 -9
- data/lib/puma/rack_default.rb +1 -1
- data/lib/puma/reactor.rb +85 -369
- data/lib/puma/request.rb +607 -0
- data/lib/puma/runner.rb +83 -77
- data/lib/puma/server.rb +305 -789
- data/lib/puma/single.rb +18 -74
- data/lib/puma/state_file.rb +45 -8
- data/lib/puma/systemd.rb +47 -0
- data/lib/puma/thread_pool.rb +137 -66
- data/lib/puma/util.rb +21 -4
- data/lib/puma.rb +54 -5
- data/lib/rack/handler/puma.rb +11 -12
- data/tools/{docker/Dockerfile → Dockerfile} +1 -1
- metadata +31 -23
- data/docs/tcp_mode.md +0 -96
- data/ext/puma_http11/io_buffer.c +0 -155
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
- data/lib/puma/accept_nonblock.rb +0 -29
- data/lib/puma/tcp_logger.rb +0 -41
- 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,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require_relative 'runner'
|
4
|
+
require_relative 'util'
|
5
|
+
require_relative 'plugin'
|
6
|
+
require_relative 'cluster/worker_handle'
|
7
|
+
require_relative 'cluster/worker'
|
6
8
|
|
7
9
|
require 'time'
|
8
10
|
|
@@ -11,25 +13,24 @@ module Puma
|
|
11
13
|
# to boot and serve a Ruby application when puma "workers" are needed
|
12
14
|
# i.e. when using multi-processes. For example `$ puma -w 5`
|
13
15
|
#
|
14
|
-
# At the core of this class is running an instance of `Puma::Server` which
|
15
|
-
# gets created via the `start_server` method from the `Puma::Runner` class
|
16
|
-
# that this inherits from.
|
17
|
-
#
|
18
16
|
# An instance of this class will spawn the number of processes passed in
|
19
17
|
# via the `spawn_workers` method call. Each worker will have it's own
|
20
18
|
# instance of a `Puma::Server`.
|
21
19
|
class Cluster < Runner
|
22
|
-
def initialize(
|
23
|
-
super
|
20
|
+
def initialize(launcher)
|
21
|
+
super(launcher)
|
24
22
|
|
25
23
|
@phase = 0
|
26
24
|
@workers = []
|
27
|
-
@next_check =
|
25
|
+
@next_check = Time.now
|
28
26
|
|
29
|
-
@phased_state = :idle
|
30
27
|
@phased_restart = false
|
31
28
|
end
|
32
29
|
|
30
|
+
# Returns the list of cluster worker handles.
|
31
|
+
# @return [Array<Puma::Cluster::WorkerHandle>]
|
32
|
+
attr_reader :workers
|
33
|
+
|
33
34
|
def stop_workers
|
34
35
|
log "- Gracefully shutting down workers..."
|
35
36
|
@workers.each { |x| x.term }
|
@@ -37,7 +38,7 @@ module Puma
|
|
37
38
|
begin
|
38
39
|
loop do
|
39
40
|
wait_workers
|
40
|
-
break if @workers.empty?
|
41
|
+
break if @workers.reject {|w| w.pid.nil?}.empty?
|
41
42
|
sleep 0.2
|
42
43
|
end
|
43
44
|
rescue Interrupt
|
@@ -46,6 +47,7 @@ module Puma
|
|
46
47
|
end
|
47
48
|
|
48
49
|
def start_phased_restart
|
50
|
+
@events.fire_on_restart!
|
49
51
|
@phase += 1
|
50
52
|
log "- Starting phased worker restart, phase: #{@phase}"
|
51
53
|
|
@@ -62,143 +64,102 @@ module Puma
|
|
62
64
|
@workers.each { |x| x.hup }
|
63
65
|
end
|
64
66
|
|
65
|
-
class Worker
|
66
|
-
def initialize(idx, pid, phase, options)
|
67
|
-
@index = idx
|
68
|
-
@pid = pid
|
69
|
-
@phase = phase
|
70
|
-
@stage = :started
|
71
|
-
@signal = "TERM"
|
72
|
-
@options = options
|
73
|
-
@first_term_sent = nil
|
74
|
-
@started_at = Time.now
|
75
|
-
@last_checkin = Time.now
|
76
|
-
@last_status = '{}'
|
77
|
-
@term = false
|
78
|
-
end
|
79
|
-
|
80
|
-
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
81
|
-
|
82
|
-
def booted?
|
83
|
-
@stage == :booted
|
84
|
-
end
|
85
|
-
|
86
|
-
def boot!
|
87
|
-
@last_checkin = Time.now
|
88
|
-
@stage = :booted
|
89
|
-
end
|
90
|
-
|
91
|
-
def term?
|
92
|
-
@term
|
93
|
-
end
|
94
|
-
|
95
|
-
def ping!(status)
|
96
|
-
@last_checkin = Time.now
|
97
|
-
@last_status = status
|
98
|
-
end
|
99
|
-
|
100
|
-
def ping_timeout?(which)
|
101
|
-
Time.now - @last_checkin > which
|
102
|
-
end
|
103
|
-
|
104
|
-
def term
|
105
|
-
begin
|
106
|
-
if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
|
107
|
-
@signal = "KILL"
|
108
|
-
else
|
109
|
-
@term ||= true
|
110
|
-
@first_term_sent ||= Time.now
|
111
|
-
end
|
112
|
-
Process.kill @signal, @pid
|
113
|
-
rescue Errno::ESRCH
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def kill
|
118
|
-
Process.kill "KILL", @pid
|
119
|
-
rescue Errno::ESRCH
|
120
|
-
end
|
121
|
-
|
122
|
-
def hup
|
123
|
-
Process.kill "HUP", @pid
|
124
|
-
rescue Errno::ESRCH
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
67
|
def spawn_workers
|
129
68
|
diff = @options[:workers] - @workers.size
|
130
69
|
return if diff < 1
|
131
70
|
|
132
71
|
master = Process.pid
|
72
|
+
if @options[:fork_worker]
|
73
|
+
@fork_writer << "-1\n"
|
74
|
+
end
|
133
75
|
|
134
76
|
diff.times do
|
135
77
|
idx = next_worker_index
|
136
|
-
@launcher.config.run_hooks :before_worker_fork, idx
|
137
78
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
79
|
+
if @options[:fork_worker] && idx != 0
|
80
|
+
@fork_writer << "#{idx}\n"
|
81
|
+
pid = nil
|
82
|
+
else
|
83
|
+
pid = spawn_worker(idx, master)
|
143
84
|
end
|
144
85
|
|
145
86
|
debug "Spawned worker: #{pid}"
|
146
|
-
@workers <<
|
87
|
+
@workers << WorkerHandle.new(idx, pid, @phase, @options)
|
88
|
+
end
|
147
89
|
|
148
|
-
|
90
|
+
if @options[:fork_worker] &&
|
91
|
+
@workers.all? {|x| x.phase == @phase}
|
92
|
+
|
93
|
+
@fork_writer << "0\n"
|
149
94
|
end
|
95
|
+
end
|
150
96
|
|
151
|
-
|
152
|
-
|
97
|
+
# @version 5.0.0
|
98
|
+
def spawn_worker(idx, master)
|
99
|
+
@config.run_hooks(:before_worker_fork, idx, @log_writer)
|
100
|
+
|
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
|
153
106
|
end
|
107
|
+
|
108
|
+
@config.run_hooks(:after_worker_fork, idx, @log_writer)
|
109
|
+
pid
|
154
110
|
end
|
155
111
|
|
156
112
|
def cull_workers
|
157
113
|
diff = @workers.size - @options[:workers]
|
158
114
|
return if diff < 1
|
115
|
+
debug "Culling #{diff} workers"
|
159
116
|
|
160
|
-
|
161
|
-
|
162
|
-
workers_to_cull = @workers[-diff,diff]
|
163
|
-
debug "Workers to cull: #{workers_to_cull.inspect}"
|
117
|
+
workers = workers_to_cull(diff)
|
118
|
+
debug "Workers to cull: #{workers.inspect}"
|
164
119
|
|
165
|
-
|
166
|
-
log "- Worker #{worker.index} (
|
120
|
+
workers.each do |worker|
|
121
|
+
log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
|
167
122
|
worker.term
|
168
123
|
end
|
169
124
|
end
|
170
125
|
|
126
|
+
def workers_to_cull(diff)
|
127
|
+
workers = @workers.sort_by(&:started_at)
|
128
|
+
|
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]
|
132
|
+
|
133
|
+
workers[cull_start_index(diff), diff]
|
134
|
+
end
|
135
|
+
|
136
|
+
def cull_start_index(diff)
|
137
|
+
case @options[:worker_culling_strategy]
|
138
|
+
when :oldest
|
139
|
+
0
|
140
|
+
else # :youngest
|
141
|
+
-diff
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @!attribute [r] next_worker_index
|
171
146
|
def next_worker_index
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
147
|
+
occupied_positions = @workers.map(&:index)
|
148
|
+
idx = 0
|
149
|
+
idx += 1 until !occupied_positions.include?(idx)
|
150
|
+
idx
|
176
151
|
end
|
177
152
|
|
178
153
|
def all_workers_booted?
|
179
154
|
@workers.count { |w| !w.booted? } == 0
|
180
155
|
end
|
181
156
|
|
182
|
-
def check_workers
|
183
|
-
return if
|
184
|
-
|
185
|
-
@next_check = Time.now + Const::WORKER_CHECK_INTERVAL
|
157
|
+
def check_workers
|
158
|
+
return if @next_check >= Time.now
|
186
159
|
|
187
|
-
|
188
|
-
|
189
|
-
@workers.each do |w|
|
190
|
-
next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
|
191
|
-
if w.ping_timeout?(@options[:worker_timeout])
|
192
|
-
log "! Terminating timed out worker: #{w.pid}"
|
193
|
-
w.kill
|
194
|
-
any = true
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# If we killed any timed out workers, try to catch them
|
199
|
-
# during this loop by giving the kernel time to kill them.
|
200
|
-
sleep 1 if any
|
160
|
+
@next_check = Time.now + @options[:worker_check_interval]
|
201
161
|
|
162
|
+
timeout_workers
|
202
163
|
wait_workers
|
203
164
|
cull_workers
|
204
165
|
spawn_workers
|
@@ -211,104 +172,40 @@ module Puma
|
|
211
172
|
w = @workers.find { |x| x.phase != @phase }
|
212
173
|
|
213
174
|
if w
|
214
|
-
|
215
|
-
@phased_state = :waiting
|
216
|
-
log "- Stopping #{w.pid} for phased upgrade..."
|
217
|
-
end
|
218
|
-
|
175
|
+
log "- Stopping #{w.pid} for phased upgrade..."
|
219
176
|
unless w.term?
|
220
177
|
w.term
|
221
178
|
log "- #{w.signal} sent to #{w.pid}..."
|
222
179
|
end
|
223
180
|
end
|
224
181
|
end
|
225
|
-
end
|
226
182
|
|
227
|
-
|
228
|
-
|
183
|
+
t = @workers.reject(&:term?)
|
184
|
+
t.map!(&:ping_timeout)
|
229
185
|
|
230
|
-
|
231
|
-
@wakeup.write "!" unless @wakeup.closed?
|
232
|
-
rescue SystemCallError, IOError
|
233
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
234
|
-
end
|
186
|
+
@next_check = [t.min, @next_check].compact.min
|
235
187
|
end
|
236
188
|
|
237
189
|
def worker(index, master)
|
238
|
-
title = "puma: cluster worker #{index}: #{master}"
|
239
|
-
title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
|
240
|
-
$0 = title
|
241
|
-
|
242
|
-
Signal.trap "SIGINT", "IGNORE"
|
243
|
-
|
244
190
|
@workers = []
|
191
|
+
|
245
192
|
@master_read.close
|
246
193
|
@suicide_pipe.close
|
194
|
+
@fork_writer.close
|
247
195
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
exit! 1
|
253
|
-
end
|
254
|
-
|
255
|
-
# If we're not running under a Bundler context, then
|
256
|
-
# report the info about the context we will be using
|
257
|
-
if !ENV['BUNDLE_GEMFILE']
|
258
|
-
if File.exist?("Gemfile")
|
259
|
-
log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
|
260
|
-
elsif File.exist?("gems.rb")
|
261
|
-
log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
# Invoke any worker boot hooks so they can get
|
266
|
-
# things in shape before booting the app.
|
267
|
-
@launcher.config.run_hooks :before_worker_boot, index
|
268
|
-
|
269
|
-
server = start_server
|
270
|
-
|
271
|
-
Signal.trap "SIGTERM" do
|
272
|
-
@worker_write << "e#{Process.pid}\n" rescue nil
|
273
|
-
server.stop
|
274
|
-
end
|
275
|
-
|
276
|
-
begin
|
277
|
-
@worker_write << "b#{Process.pid}\n"
|
278
|
-
rescue SystemCallError, IOError
|
279
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
280
|
-
STDERR.puts "Master seems to have exited, exiting."
|
281
|
-
return
|
282
|
-
end
|
283
|
-
|
284
|
-
Thread.new(@worker_write) do |io|
|
285
|
-
Puma.set_thread_name "stat payload"
|
286
|
-
base_payload = "p#{Process.pid}"
|
287
|
-
|
288
|
-
while true
|
289
|
-
sleep Const::WORKER_CHECK_INTERVAL
|
290
|
-
begin
|
291
|
-
b = server.backlog || 0
|
292
|
-
r = server.running || 0
|
293
|
-
t = server.pool_capacity || 0
|
294
|
-
m = server.max_threads || 0
|
295
|
-
payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
|
296
|
-
io << payload
|
297
|
-
rescue IOError
|
298
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
299
|
-
break
|
300
|
-
end
|
301
|
-
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
|
302
200
|
end
|
303
201
|
|
304
|
-
server
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
@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
|
312
209
|
end
|
313
210
|
|
314
211
|
def restart
|
@@ -316,8 +213,8 @@ module Puma
|
|
316
213
|
stop
|
317
214
|
end
|
318
215
|
|
319
|
-
def phased_restart
|
320
|
-
return false if @options[:preload_app]
|
216
|
+
def phased_restart(refork = false)
|
217
|
+
return false if @options[:preload_app] && !refork
|
321
218
|
|
322
219
|
@phased_restart = true
|
323
220
|
wakeup!
|
@@ -333,7 +230,7 @@ module Puma
|
|
333
230
|
def stop_blocked
|
334
231
|
@status = :stop if @status == :run
|
335
232
|
wakeup!
|
336
|
-
@control
|
233
|
+
@control&.stop true
|
337
234
|
Process.waitall
|
338
235
|
end
|
339
236
|
|
@@ -350,20 +247,61 @@ module Puma
|
|
350
247
|
|
351
248
|
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
352
249
|
# the master process.
|
250
|
+
# @!attribute [r] stats
|
353
251
|
def stats
|
354
252
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
355
|
-
|
356
|
-
|
357
|
-
|
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)
|
358
273
|
end
|
359
274
|
|
360
275
|
def preload?
|
361
276
|
@options[:preload_app]
|
362
277
|
end
|
363
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
|
+
|
364
287
|
# We do this in a separate method to keep the lambda scope
|
365
288
|
# of the signals handlers as small as possible.
|
366
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
|
+
|
367
305
|
Signal.trap "SIGCHLD" do
|
368
306
|
wakeup!
|
369
307
|
end
|
@@ -392,7 +330,7 @@ module Puma
|
|
392
330
|
|
393
331
|
stop_workers
|
394
332
|
stop
|
395
|
-
|
333
|
+
@events.fire_on_stopped!
|
396
334
|
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
397
335
|
exit 0 # Clean exit, workers were stopped
|
398
336
|
end
|
@@ -404,15 +342,25 @@ module Puma
|
|
404
342
|
|
405
343
|
output_header "cluster"
|
406
344
|
|
407
|
-
|
408
|
-
|
409
|
-
before = Thread.list
|
345
|
+
# This is aligned with the output from Runner, see Runner#output_header
|
346
|
+
log "* Workers: #{@options[:workers]}"
|
410
347
|
|
411
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"
|
412
360
|
log "* Preloading application"
|
413
361
|
load_and_bind
|
414
362
|
|
415
|
-
after = Thread.list
|
363
|
+
after = Thread.list.reject(&fork_safe)
|
416
364
|
|
417
365
|
if after.size > before.size
|
418
366
|
threads = (after - before)
|
@@ -426,14 +374,14 @@ module Puma
|
|
426
374
|
end
|
427
375
|
end
|
428
376
|
else
|
429
|
-
log "*
|
377
|
+
log "* Restarts: (\u2714) hot (\u2714) phased"
|
430
378
|
|
431
|
-
unless @
|
379
|
+
unless @config.app_configured?
|
432
380
|
error "No application configured, nothing to run"
|
433
381
|
exit 1
|
434
382
|
end
|
435
383
|
|
436
|
-
@launcher.binder.parse @options[:binds]
|
384
|
+
@launcher.binder.parse @options[:binds]
|
437
385
|
end
|
438
386
|
|
439
387
|
read, @wakeup = Puma::Util.pipe
|
@@ -447,12 +395,13 @@ module Puma
|
|
447
395
|
#
|
448
396
|
@check_pipe, @suicide_pipe = Puma::Util.pipe
|
449
397
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
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
|
456
405
|
|
457
406
|
redirect_io
|
458
407
|
|
@@ -464,7 +413,7 @@ module Puma
|
|
464
413
|
|
465
414
|
@master_read, @worker_write = read, @wakeup
|
466
415
|
|
467
|
-
@
|
416
|
+
@config.run_hooks(:before_fork, nil, @log_writer)
|
468
417
|
|
469
418
|
spawn_workers
|
470
419
|
|
@@ -472,51 +421,65 @@ module Puma
|
|
472
421
|
stop
|
473
422
|
end
|
474
423
|
|
475
|
-
@launcher.events.fire_on_booted!
|
476
|
-
|
477
424
|
begin
|
478
|
-
|
425
|
+
booted = false
|
426
|
+
in_phased_restart = false
|
427
|
+
workers_not_booted = @options[:workers]
|
479
428
|
|
480
429
|
while @status == :run
|
481
430
|
begin
|
482
431
|
if @phased_restart
|
483
432
|
start_phased_restart
|
484
433
|
@phased_restart = false
|
434
|
+
in_phased_restart = true
|
435
|
+
workers_not_booted = @options[:workers]
|
485
436
|
end
|
486
437
|
|
487
|
-
check_workers
|
438
|
+
check_workers
|
488
439
|
|
489
|
-
|
490
|
-
|
491
|
-
res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
|
492
|
-
|
493
|
-
if res
|
440
|
+
if read.wait_readable([0, @next_check - Time.now].max)
|
494
441
|
req = read.read_nonblock(1)
|
495
442
|
|
443
|
+
@next_check = Time.now if req == "!"
|
496
444
|
next if !req || req == "!"
|
497
445
|
|
498
446
|
result = read.gets
|
499
447
|
pid = result.to_i
|
500
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
|
+
|
501
455
|
if w = @workers.find { |x| x.pid == pid }
|
502
456
|
case req
|
503
457
|
when "b"
|
504
458
|
w.boot!
|
505
|
-
log "- Worker #{w.index} (
|
506
|
-
|
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
|
507
462
|
when "e"
|
508
463
|
# external term, see worker method, Signal.trap "SIGTERM"
|
509
|
-
w.
|
464
|
+
w.term!
|
510
465
|
when "t"
|
511
466
|
w.term unless w.term?
|
512
|
-
force_check = true
|
513
467
|
when "p"
|
514
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
|
515
474
|
end
|
516
475
|
else
|
517
476
|
log "! Out-of-sync worker list, no #{pid} worker"
|
518
477
|
end
|
519
478
|
end
|
479
|
+
if in_phased_restart && workers_not_booted.zero?
|
480
|
+
@events.fire_on_booted!
|
481
|
+
in_phased_restart = false
|
482
|
+
end
|
520
483
|
|
521
484
|
rescue Interrupt
|
522
485
|
@status = :stop
|
@@ -534,10 +497,20 @@ module Puma
|
|
534
497
|
|
535
498
|
private
|
536
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
|
+
|
537
509
|
# loops thru @workers, removing workers that exited, and calling
|
538
510
|
# `#term` if needed
|
539
511
|
def wait_workers
|
540
512
|
@workers.reject! do |w|
|
513
|
+
next false if w.pid.nil?
|
541
514
|
begin
|
542
515
|
if Process.wait(w.pid, Process::WNOHANG)
|
543
516
|
true
|
@@ -546,7 +519,29 @@ module Puma
|
|
546
519
|
nil
|
547
520
|
end
|
548
521
|
rescue Errno::ECHILD
|
549
|
-
|
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
|
550
545
|
end
|
551
546
|
end
|
552
547
|
end
|
data/lib/puma/commonlogger.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Puma
|
4
4
|
# Rack::CommonLogger forwards every request to the given +app+, and
|
5
5
|
# logs a line in the
|
6
|
-
# {Apache common log format}[
|
6
|
+
# {Apache common log format}[https://httpd.apache.org/docs/1.3/logs.html#common]
|
7
7
|
# to the +logger+.
|
8
8
|
#
|
9
9
|
# If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
|
@@ -16,7 +16,7 @@ module Puma
|
|
16
16
|
# (which is called without arguments in order to make the error appear for
|
17
17
|
# sure)
|
18
18
|
class CommonLogger
|
19
|
-
# Common Log Format:
|
19
|
+
# Common Log Format: https://httpd.apache.org/docs/1.3/logs.html#common
|
20
20
|
#
|
21
21
|
# lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
|
22
22
|
#
|