puma 6.4.0 → 6.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/History.md +235 -18
- data/README.md +87 -26
- data/docs/fork_worker.md +11 -1
- data/docs/java_options.md +54 -0
- data/docs/kubernetes.md +1 -1
- data/docs/plugins.md +4 -0
- data/docs/restart.md +1 -0
- data/docs/signals.md +2 -2
- data/docs/stats.md +8 -3
- data/docs/systemd.md +13 -6
- data/ext/puma_http11/extconf.rb +21 -14
- data/ext/puma_http11/mini_ssl.c +16 -2
- data/ext/puma_http11/org/jruby/puma/Http11.java +31 -8
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +2 -1
- data/ext/puma_http11/puma_http11.c +4 -1
- data/lib/puma/app/status.rb +1 -1
- data/lib/puma/binder.rb +12 -4
- data/lib/puma/cli.rb +9 -5
- data/lib/puma/client.rb +104 -16
- data/lib/puma/cluster/worker.rb +9 -6
- data/lib/puma/cluster/worker_handle.rb +4 -5
- data/lib/puma/cluster.rb +112 -27
- data/lib/puma/configuration.rb +37 -21
- data/lib/puma/const.rb +22 -3
- data/lib/puma/control_cli.rb +4 -4
- data/lib/puma/detect.rb +3 -4
- data/lib/puma/dsl.rb +303 -58
- data/lib/puma/error_logger.rb +4 -4
- data/lib/puma/jruby_restart.rb +0 -16
- data/lib/puma/launcher.rb +21 -8
- data/lib/puma/log_writer.rb +9 -9
- data/lib/puma/minissl/context_builder.rb +3 -0
- data/lib/puma/minissl.rb +6 -0
- data/lib/puma/null_io.rb +42 -2
- data/lib/puma/request.rb +35 -14
- data/lib/puma/runner.rb +10 -3
- data/lib/puma/sd_notify.rb +1 -4
- data/lib/puma/server.rb +53 -21
- data/lib/puma/single.rb +1 -1
- data/lib/puma/state_file.rb +2 -2
- data/lib/puma/thread_pool.rb +14 -2
- data/lib/puma/util.rb +1 -1
- data/lib/rack/handler/puma.rb +8 -5
- data/tools/Dockerfile +5 -3
- metadata +10 -12
data/lib/puma/client.rb
CHANGED
|
@@ -51,6 +51,14 @@ module Puma
|
|
|
51
51
|
CHUNK_VALID_ENDING = Const::LINE_END
|
|
52
52
|
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
|
53
53
|
|
|
54
|
+
# The maximum number of bytes we'll buffer looking for a valid
|
|
55
|
+
# chunk header.
|
|
56
|
+
MAX_CHUNK_HEADER_SIZE = 4096
|
|
57
|
+
|
|
58
|
+
# The maximum amount of excess data the client sends
|
|
59
|
+
# using chunk size extensions before we abort the connection.
|
|
60
|
+
MAX_CHUNK_EXCESS = 16 * 1024
|
|
61
|
+
|
|
54
62
|
# Content-Length header value validation
|
|
55
63
|
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
|
56
64
|
|
|
@@ -152,8 +160,6 @@ module Puma
|
|
|
152
160
|
@read_header = true
|
|
153
161
|
@read_proxy = !!@expect_proxy_proto
|
|
154
162
|
@env = @proto_env.dup
|
|
155
|
-
@body = nil
|
|
156
|
-
@tempfile = nil
|
|
157
163
|
@parsed_bytes = 0
|
|
158
164
|
@ready = false
|
|
159
165
|
@body_remain = 0
|
|
@@ -164,7 +170,7 @@ module Puma
|
|
|
164
170
|
if @buffer
|
|
165
171
|
return false unless try_to_parse_proxy_protocol
|
|
166
172
|
|
|
167
|
-
@parsed_bytes =
|
|
173
|
+
@parsed_bytes = parser_execute
|
|
168
174
|
|
|
169
175
|
if @parser.finished?
|
|
170
176
|
return setup_body
|
|
@@ -182,11 +188,11 @@ module Puma
|
|
|
182
188
|
rescue IOError
|
|
183
189
|
# swallow it
|
|
184
190
|
end
|
|
185
|
-
|
|
186
191
|
end
|
|
187
192
|
end
|
|
188
193
|
|
|
189
194
|
def close
|
|
195
|
+
tempfile_close
|
|
190
196
|
begin
|
|
191
197
|
@io.close
|
|
192
198
|
rescue IOError, Errno::EBADF
|
|
@@ -194,6 +200,15 @@ module Puma
|
|
|
194
200
|
end
|
|
195
201
|
end
|
|
196
202
|
|
|
203
|
+
def tempfile_close
|
|
204
|
+
tf_path = @tempfile&.path
|
|
205
|
+
@tempfile&.close
|
|
206
|
+
File.unlink(tf_path) if tf_path
|
|
207
|
+
@tempfile = nil
|
|
208
|
+
@body = nil
|
|
209
|
+
rescue Errno::ENOENT, IOError
|
|
210
|
+
end
|
|
211
|
+
|
|
197
212
|
# If necessary, read the PROXY protocol from the buffer. Returns
|
|
198
213
|
# false if more data is needed.
|
|
199
214
|
def try_to_parse_proxy_protocol
|
|
@@ -232,6 +247,7 @@ module Puma
|
|
|
232
247
|
|
|
233
248
|
return read_body if in_data_phase
|
|
234
249
|
|
|
250
|
+
data = nil
|
|
235
251
|
begin
|
|
236
252
|
data = @io.read_nonblock(CHUNK_SIZE)
|
|
237
253
|
rescue IO::WaitReadable
|
|
@@ -257,20 +273,20 @@ module Puma
|
|
|
257
273
|
|
|
258
274
|
return false unless try_to_parse_proxy_protocol
|
|
259
275
|
|
|
260
|
-
@parsed_bytes =
|
|
276
|
+
@parsed_bytes = parser_execute
|
|
261
277
|
|
|
262
278
|
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
|
263
279
|
@http_content_length_limit_exceeded = true
|
|
264
280
|
end
|
|
265
281
|
|
|
266
282
|
if @parser.finished?
|
|
267
|
-
|
|
283
|
+
setup_body
|
|
268
284
|
elsif @parsed_bytes >= MAX_HEADER
|
|
269
285
|
raise HttpParserError,
|
|
270
286
|
"HEADER is longer than allowed, aborting client early."
|
|
287
|
+
else
|
|
288
|
+
false
|
|
271
289
|
end
|
|
272
|
-
|
|
273
|
-
false
|
|
274
290
|
end
|
|
275
291
|
|
|
276
292
|
def eagerly_finish
|
|
@@ -284,6 +300,44 @@ module Puma
|
|
|
284
300
|
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
|
285
301
|
end
|
|
286
302
|
|
|
303
|
+
# Wraps `@parser.execute` and adds meaningful error messages
|
|
304
|
+
# @return [Integer] bytes of buffer read by parser
|
|
305
|
+
#
|
|
306
|
+
def parser_execute
|
|
307
|
+
@parser.execute(@env, @buffer, @parsed_bytes)
|
|
308
|
+
rescue => e
|
|
309
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
310
|
+
raise e unless HttpParserError === e && e.message.include?('non-SSL')
|
|
311
|
+
|
|
312
|
+
req, _ = @buffer.split "\r\n\r\n"
|
|
313
|
+
request_line, headers = req.split "\r\n", 2
|
|
314
|
+
|
|
315
|
+
# below checks for request issues and changes error message accordingly
|
|
316
|
+
if !@env.key? REQUEST_METHOD
|
|
317
|
+
if request_line.count(' ') != 2
|
|
318
|
+
# maybe this is an SSL connection ?
|
|
319
|
+
raise e
|
|
320
|
+
else
|
|
321
|
+
method = request_line[/\A[^ ]+/]
|
|
322
|
+
raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
|
|
323
|
+
end
|
|
324
|
+
elsif !@env.key? REQUEST_PATH
|
|
325
|
+
path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
|
|
326
|
+
raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
|
|
327
|
+
elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
|
|
328
|
+
query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
|
|
329
|
+
raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
|
|
330
|
+
elsif !@env.key? SERVER_PROTOCOL
|
|
331
|
+
# protocol is bad
|
|
332
|
+
text = request_line[/[^ ]*\z/]
|
|
333
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
|
|
334
|
+
elsif !headers.empty?
|
|
335
|
+
# headers are bad
|
|
336
|
+
hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
|
|
337
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
287
341
|
def timeout!
|
|
288
342
|
write_error(408) if in_data_phase
|
|
289
343
|
raise ConnectionError
|
|
@@ -395,18 +449,33 @@ module Puma
|
|
|
395
449
|
return true
|
|
396
450
|
end
|
|
397
451
|
|
|
398
|
-
|
|
452
|
+
content_length = cl.to_i
|
|
453
|
+
|
|
454
|
+
remain = content_length - body.bytesize
|
|
399
455
|
|
|
400
456
|
if remain <= 0
|
|
401
|
-
|
|
402
|
-
|
|
457
|
+
# Part of the body is a pipelined request OR garbage. We'll deal with that later.
|
|
458
|
+
if content_length == 0
|
|
459
|
+
@body = EmptyBody
|
|
460
|
+
if body.empty?
|
|
461
|
+
@buffer = nil
|
|
462
|
+
else
|
|
463
|
+
@buffer = body
|
|
464
|
+
end
|
|
465
|
+
elsif remain == 0
|
|
466
|
+
@body = StringIO.new body
|
|
467
|
+
@buffer = nil
|
|
468
|
+
else
|
|
469
|
+
@body = StringIO.new(body[0,content_length])
|
|
470
|
+
@buffer = body[content_length..-1]
|
|
471
|
+
end
|
|
403
472
|
set_ready
|
|
404
473
|
return true
|
|
405
474
|
end
|
|
406
475
|
|
|
407
476
|
if remain > MAX_BODY
|
|
408
|
-
@body = Tempfile.
|
|
409
|
-
@body.
|
|
477
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
478
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
410
479
|
@body.binmode
|
|
411
480
|
@tempfile = @body
|
|
412
481
|
else
|
|
@@ -470,7 +539,7 @@ module Puma
|
|
|
470
539
|
def read_chunked_body
|
|
471
540
|
while true
|
|
472
541
|
begin
|
|
473
|
-
chunk = @io.read_nonblock(
|
|
542
|
+
chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
|
|
474
543
|
rescue IO::WaitReadable
|
|
475
544
|
return false
|
|
476
545
|
rescue SystemCallError, IOError
|
|
@@ -496,9 +565,10 @@ module Puma
|
|
|
496
565
|
@chunked_body = true
|
|
497
566
|
@partial_part_left = 0
|
|
498
567
|
@prev_chunk = ""
|
|
568
|
+
@excess_cr = 0
|
|
499
569
|
|
|
500
|
-
@body = Tempfile.
|
|
501
|
-
@body.
|
|
570
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
571
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
502
572
|
@body.binmode
|
|
503
573
|
@tempfile = @body
|
|
504
574
|
@chunked_content_length = 0
|
|
@@ -577,6 +647,20 @@ module Puma
|
|
|
577
647
|
end
|
|
578
648
|
end
|
|
579
649
|
|
|
650
|
+
# Track the excess as a function of the size of the
|
|
651
|
+
# header vs the size of the actual data. Excess can
|
|
652
|
+
# go negative (and is expected to) when the body is
|
|
653
|
+
# significant.
|
|
654
|
+
# The additional of chunk_hex.size and 2 compensates
|
|
655
|
+
# for a client sending 1 byte in a chunked body over
|
|
656
|
+
# a long period of time, making sure that that client
|
|
657
|
+
# isn't accidentally eventually punished.
|
|
658
|
+
@excess_cr += (line.size - len - chunk_hex.size - 2)
|
|
659
|
+
|
|
660
|
+
if @excess_cr >= MAX_CHUNK_EXCESS
|
|
661
|
+
raise HttpParserError, "Maximum chunk excess detected"
|
|
662
|
+
end
|
|
663
|
+
|
|
580
664
|
len += 2
|
|
581
665
|
|
|
582
666
|
part = io.read(len)
|
|
@@ -604,6 +688,10 @@ module Puma
|
|
|
604
688
|
@partial_part_left = len - part.size
|
|
605
689
|
end
|
|
606
690
|
else
|
|
691
|
+
if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
|
|
692
|
+
raise HttpParserError, "maximum size of chunk header exceeded"
|
|
693
|
+
end
|
|
694
|
+
|
|
607
695
|
@prev_chunk = line
|
|
608
696
|
return false
|
|
609
697
|
end
|
data/lib/puma/cluster/worker.rb
CHANGED
|
@@ -88,25 +88,27 @@ module Puma
|
|
|
88
88
|
server.begin_restart(true)
|
|
89
89
|
@config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
|
|
90
90
|
end
|
|
91
|
+
elsif idx == -2 # refork cycle is done
|
|
92
|
+
@config.run_hooks(:after_refork, nil, @log_writer, @hook_data)
|
|
91
93
|
elsif idx == 0 # restart server
|
|
92
94
|
restart_server << true << false
|
|
93
95
|
else # fork worker
|
|
94
96
|
worker_pids << pid = spawn_worker(idx)
|
|
95
|
-
@worker_write << "
|
|
97
|
+
@worker_write << "#{PIPE_FORK}#{pid}:#{idx}\n" rescue nil
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
end
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
Signal.trap "SIGTERM" do
|
|
102
|
-
@worker_write << "
|
|
104
|
+
@worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
|
|
103
105
|
restart_server.clear
|
|
104
106
|
server.stop
|
|
105
107
|
restart_server << false
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
begin
|
|
109
|
-
@worker_write << "
|
|
111
|
+
@worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
|
|
110
112
|
rescue SystemCallError, IOError
|
|
111
113
|
Puma::Util.purge_interrupt_queue
|
|
112
114
|
STDERR.puts "Master seems to have exited, exiting."
|
|
@@ -122,7 +124,7 @@ module Puma
|
|
|
122
124
|
|
|
123
125
|
stat_thread ||= Thread.new(@worker_write) do |io|
|
|
124
126
|
Puma.set_thread_name "stat pld"
|
|
125
|
-
base_payload = "
|
|
127
|
+
base_payload = "#{PIPE_PING}#{Process.pid}"
|
|
126
128
|
|
|
127
129
|
while true
|
|
128
130
|
begin
|
|
@@ -131,7 +133,8 @@ module Puma
|
|
|
131
133
|
t = server.pool_capacity || 0
|
|
132
134
|
m = server.max_threads || 0
|
|
133
135
|
rc = server.requests_count || 0
|
|
134
|
-
|
|
136
|
+
bt = server.busy_threads || 0
|
|
137
|
+
payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads":#{m}, "requests_count":#{rc}, "busy_threads":#{bt} }\n!
|
|
135
138
|
io << payload
|
|
136
139
|
rescue IOError
|
|
137
140
|
Puma::Util.purge_interrupt_queue
|
|
@@ -147,7 +150,7 @@ module Puma
|
|
|
147
150
|
# exiting until any background operations are completed
|
|
148
151
|
@config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
|
|
149
152
|
ensure
|
|
150
|
-
@worker_write << "
|
|
153
|
+
@worker_write << "#{PIPE_TERM}#{Process.pid}\n" rescue nil
|
|
151
154
|
@worker_write.close
|
|
152
155
|
end
|
|
153
156
|
|
|
@@ -51,13 +51,12 @@ module Puma
|
|
|
51
51
|
@term
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
STATUS_PATTERN = /{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads":(?<max_threads>\d*), "requests_count":(?<requests_count>\d*), "busy_threads":(?<busy_threads>\d*) }/
|
|
55
|
+
private_constant :STATUS_PATTERN
|
|
56
|
+
|
|
54
57
|
def ping!(status)
|
|
55
58
|
@last_checkin = Time.now
|
|
56
|
-
|
|
57
|
-
@last_status = captures.names.inject({}) do |hash, key|
|
|
58
|
-
hash[key.to_sym] = captures[key].to_i
|
|
59
|
-
hash
|
|
60
|
-
end
|
|
59
|
+
@last_status = status.match(STATUS_PATTERN).named_captures.map { |c_name, c| [c_name.to_sym, c.to_i] }.to_h
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
# @see Puma::Cluster#check_workers
|
data/lib/puma/cluster.rb
CHANGED
|
@@ -44,10 +44,15 @@ module Puma
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def start_phased_restart
|
|
47
|
+
def start_phased_restart(refork = false)
|
|
48
48
|
@events.fire_on_restart!
|
|
49
|
+
|
|
49
50
|
@phase += 1
|
|
50
|
-
|
|
51
|
+
if refork
|
|
52
|
+
log "- Starting worker refork, phase: #{@phase}"
|
|
53
|
+
else
|
|
54
|
+
log "- Starting phased worker restart, phase: #{@phase}"
|
|
55
|
+
end
|
|
51
56
|
|
|
52
57
|
# Be sure to change the directory again before loading
|
|
53
58
|
# the app. This way we can pick up new code.
|
|
@@ -85,10 +90,12 @@ module Puma
|
|
|
85
90
|
@workers << WorkerHandle.new(idx, pid, @phase, @options)
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
if @options[:fork_worker] &&
|
|
89
|
-
@workers.all? {|x| x.phase == @phase}
|
|
90
|
-
|
|
93
|
+
if @options[:fork_worker] && all_workers_in_phase?
|
|
91
94
|
@fork_writer << "0\n"
|
|
95
|
+
|
|
96
|
+
if worker_at(0).phase > 0
|
|
97
|
+
@fork_writer << "-2\n"
|
|
98
|
+
end
|
|
92
99
|
end
|
|
93
100
|
end
|
|
94
101
|
|
|
@@ -148,11 +155,23 @@ module Puma
|
|
|
148
155
|
idx
|
|
149
156
|
end
|
|
150
157
|
|
|
158
|
+
def worker_at(idx)
|
|
159
|
+
@workers.find { |w| w.index == idx }
|
|
160
|
+
end
|
|
161
|
+
|
|
151
162
|
def all_workers_booted?
|
|
152
163
|
@workers.count { |w| !w.booted? } == 0
|
|
153
164
|
end
|
|
154
165
|
|
|
155
|
-
def
|
|
166
|
+
def all_workers_in_phase?
|
|
167
|
+
@workers.all? { |w| w.phase == @phase }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def all_workers_idle_timed_out?
|
|
171
|
+
(@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def check_workers(refork = false)
|
|
156
175
|
return if @next_check >= Time.now
|
|
157
176
|
|
|
158
177
|
@next_check = Time.now + @options[:worker_check_interval]
|
|
@@ -170,7 +189,12 @@ module Puma
|
|
|
170
189
|
w = @workers.find { |x| x.phase != @phase }
|
|
171
190
|
|
|
172
191
|
if w
|
|
173
|
-
|
|
192
|
+
if refork
|
|
193
|
+
log "- Stopping #{w.pid} for refork..."
|
|
194
|
+
else
|
|
195
|
+
log "- Stopping #{w.pid} for phased upgrade..."
|
|
196
|
+
end
|
|
197
|
+
|
|
174
198
|
unless w.term?
|
|
175
199
|
w.term
|
|
176
200
|
log "- #{w.signal} sent to #{w.pid}..."
|
|
@@ -214,7 +238,7 @@ module Puma
|
|
|
214
238
|
def phased_restart(refork = false)
|
|
215
239
|
return false if @options[:preload_app] && !refork
|
|
216
240
|
|
|
217
|
-
@phased_restart = true
|
|
241
|
+
@phased_restart = refork ? :refork : true
|
|
218
242
|
wakeup!
|
|
219
243
|
|
|
220
244
|
true
|
|
@@ -276,7 +300,7 @@ module Puma
|
|
|
276
300
|
|
|
277
301
|
# @version 5.0.0
|
|
278
302
|
def fork_worker!
|
|
279
|
-
if (worker =
|
|
303
|
+
if (worker = worker_at 0)
|
|
280
304
|
worker.phase += 1
|
|
281
305
|
end
|
|
282
306
|
phased_restart(true)
|
|
@@ -354,7 +378,7 @@ module Puma
|
|
|
354
378
|
|
|
355
379
|
before = Thread.list.reject(&fork_safe)
|
|
356
380
|
|
|
357
|
-
log "* Restarts: (\u2714) hot (\u2716) phased"
|
|
381
|
+
log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
358
382
|
log "* Preloading application"
|
|
359
383
|
load_and_bind
|
|
360
384
|
|
|
@@ -372,7 +396,7 @@ module Puma
|
|
|
372
396
|
end
|
|
373
397
|
end
|
|
374
398
|
else
|
|
375
|
-
log "* Restarts: (\u2714) hot (\u2714) phased"
|
|
399
|
+
log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
376
400
|
|
|
377
401
|
unless @config.app_configured?
|
|
378
402
|
error "No application configured, nothing to run"
|
|
@@ -411,6 +435,8 @@ module Puma
|
|
|
411
435
|
|
|
412
436
|
@master_read, @worker_write = read, @wakeup
|
|
413
437
|
|
|
438
|
+
@options[:worker_write] = @worker_write
|
|
439
|
+
|
|
414
440
|
@config.run_hooks(:before_fork, nil, @log_writer)
|
|
415
441
|
|
|
416
442
|
spawn_workers
|
|
@@ -426,61 +452,86 @@ module Puma
|
|
|
426
452
|
|
|
427
453
|
while @status == :run
|
|
428
454
|
begin
|
|
455
|
+
if @options[:idle_timeout] && all_workers_idle_timed_out?
|
|
456
|
+
log "- All workers reached idle timeout"
|
|
457
|
+
break
|
|
458
|
+
end
|
|
459
|
+
|
|
429
460
|
if @phased_restart
|
|
430
|
-
start_phased_restart
|
|
461
|
+
start_phased_restart(@phased_restart == :refork)
|
|
462
|
+
|
|
463
|
+
in_phased_restart = @phased_restart
|
|
431
464
|
@phased_restart = false
|
|
432
|
-
|
|
465
|
+
|
|
433
466
|
workers_not_booted = @options[:workers]
|
|
467
|
+
# worker 0 is not restarted on refork
|
|
468
|
+
workers_not_booted -= 1 if in_phased_restart == :refork
|
|
434
469
|
end
|
|
435
470
|
|
|
436
|
-
check_workers
|
|
471
|
+
check_workers(in_phased_restart == :refork)
|
|
437
472
|
|
|
438
473
|
if read.wait_readable([0, @next_check - Time.now].max)
|
|
439
474
|
req = read.read_nonblock(1)
|
|
475
|
+
next unless req
|
|
440
476
|
|
|
441
|
-
|
|
442
|
-
|
|
477
|
+
if req == PIPE_WAKEUP
|
|
478
|
+
@next_check = Time.now
|
|
479
|
+
next
|
|
480
|
+
end
|
|
443
481
|
|
|
444
482
|
result = read.gets
|
|
445
483
|
pid = result.to_i
|
|
446
484
|
|
|
447
|
-
if req ==
|
|
485
|
+
if req == PIPE_BOOT || req == PIPE_FORK
|
|
448
486
|
pid, idx = result.split(':').map(&:to_i)
|
|
449
|
-
w =
|
|
487
|
+
w = worker_at idx
|
|
450
488
|
w.pid = pid if w.pid.nil?
|
|
451
489
|
end
|
|
452
490
|
|
|
453
491
|
if w = @workers.find { |x| x.pid == pid }
|
|
454
492
|
case req
|
|
455
|
-
when
|
|
493
|
+
when PIPE_BOOT
|
|
456
494
|
w.boot!
|
|
457
495
|
log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
|
|
458
496
|
@next_check = Time.now
|
|
459
497
|
workers_not_booted -= 1
|
|
460
|
-
when
|
|
498
|
+
when PIPE_EXTERNAL_TERM
|
|
461
499
|
# external term, see worker method, Signal.trap "SIGTERM"
|
|
462
500
|
w.term!
|
|
463
|
-
when
|
|
501
|
+
when PIPE_TERM
|
|
464
502
|
w.term unless w.term?
|
|
465
|
-
when
|
|
466
|
-
|
|
503
|
+
when PIPE_PING
|
|
504
|
+
status = result.sub(/^\d+/,'').chomp
|
|
505
|
+
w.ping!(status)
|
|
467
506
|
@events.fire(:ping!, w)
|
|
507
|
+
|
|
508
|
+
if in_phased_restart && @options[:fork_worker] && workers_not_booted.positive? && w0 = worker_at(0)
|
|
509
|
+
w0.ping!(status)
|
|
510
|
+
@events.fire(:ping!, w0)
|
|
511
|
+
end
|
|
512
|
+
|
|
468
513
|
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
|
469
514
|
@events.fire_on_booted!
|
|
470
515
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
471
516
|
booted = true
|
|
472
517
|
end
|
|
518
|
+
when PIPE_IDLE
|
|
519
|
+
if idle_workers[pid]
|
|
520
|
+
idle_workers.delete pid
|
|
521
|
+
else
|
|
522
|
+
idle_workers[pid] = true
|
|
523
|
+
end
|
|
473
524
|
end
|
|
474
525
|
else
|
|
475
526
|
log "! Out-of-sync worker list, no #{pid} worker"
|
|
476
527
|
end
|
|
477
528
|
end
|
|
529
|
+
|
|
478
530
|
if in_phased_restart && workers_not_booted.zero?
|
|
479
531
|
@events.fire_on_booted!
|
|
480
532
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
481
533
|
in_phased_restart = false
|
|
482
534
|
end
|
|
483
|
-
|
|
484
535
|
rescue Interrupt
|
|
485
536
|
@status = :stop
|
|
486
537
|
end
|
|
@@ -509,10 +560,31 @@ module Puma
|
|
|
509
560
|
# loops thru @workers, removing workers that exited, and calling
|
|
510
561
|
# `#term` if needed
|
|
511
562
|
def wait_workers
|
|
563
|
+
# Reap all children, known workers or otherwise.
|
|
564
|
+
# If puma has PID 1, as it's common in containerized environments,
|
|
565
|
+
# then it's responsible for reaping orphaned processes, so we must reap
|
|
566
|
+
# all our dead children, regardless of whether they are workers we spawned
|
|
567
|
+
# or some reattached processes.
|
|
568
|
+
reaped_children = {}
|
|
569
|
+
loop do
|
|
570
|
+
begin
|
|
571
|
+
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
572
|
+
break unless pid
|
|
573
|
+
reaped_children[pid] = status
|
|
574
|
+
rescue Errno::ECHILD
|
|
575
|
+
break
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
512
579
|
@workers.reject! do |w|
|
|
513
580
|
next false if w.pid.nil?
|
|
514
581
|
begin
|
|
515
|
-
|
|
582
|
+
# We may need to check the PID individually because:
|
|
583
|
+
# 1. From Ruby versions 2.6 to 3.2, `Process.detach` can prevent or delay
|
|
584
|
+
# `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
|
|
585
|
+
# 2. When `fork_worker` is enabled, some worker may not be direct children,
|
|
586
|
+
# but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
|
|
587
|
+
if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
|
|
516
588
|
true
|
|
517
589
|
else
|
|
518
590
|
w.term if w.term?
|
|
@@ -529,6 +601,11 @@ module Puma
|
|
|
529
601
|
end
|
|
530
602
|
end
|
|
531
603
|
end
|
|
604
|
+
|
|
605
|
+
# Log unknown children
|
|
606
|
+
reaped_children.each do |pid, status|
|
|
607
|
+
log "! reaped unknown child process pid=#{pid} status=#{status}"
|
|
608
|
+
end
|
|
532
609
|
end
|
|
533
610
|
|
|
534
611
|
# @version 5.0.0
|
|
@@ -536,14 +613,22 @@ module Puma
|
|
|
536
613
|
@workers.each do |w|
|
|
537
614
|
if !w.term? && w.ping_timeout <= Time.now
|
|
538
615
|
details = if w.booted?
|
|
539
|
-
"(
|
|
616
|
+
"(Worker #{w.index} failed to check in within #{@options[:worker_timeout]} seconds)"
|
|
540
617
|
else
|
|
541
|
-
"(
|
|
618
|
+
"(Worker #{w.index} failed to boot within #{@options[:worker_boot_timeout]} seconds)"
|
|
542
619
|
end
|
|
543
620
|
log "! Terminating timed out worker #{details}: #{w.pid}"
|
|
544
621
|
w.kill
|
|
545
622
|
end
|
|
546
623
|
end
|
|
547
624
|
end
|
|
625
|
+
|
|
626
|
+
def idle_timed_out_worker_pids
|
|
627
|
+
idle_workers.keys
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def idle_workers
|
|
631
|
+
@idle_workers ||= {}
|
|
632
|
+
end
|
|
548
633
|
end
|
|
549
634
|
end
|