puma 6.6.0 → 7.2.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 +248 -5
- data/README.md +40 -40
- data/docs/deployment.md +58 -23
- data/docs/fork_worker.md +5 -5
- data/docs/jungle/README.md +1 -1
- data/docs/kubernetes.md +11 -16
- data/docs/plugins.md +2 -2
- data/docs/restart.md +2 -2
- data/docs/signals.md +21 -21
- data/docs/stats.md +4 -3
- data/docs/systemd.md +4 -4
- data/ext/puma_http11/extconf.rb +2 -17
- data/ext/puma_http11/mini_ssl.c +18 -8
- data/ext/puma_http11/org/jruby/puma/Http11.java +10 -2
- data/ext/puma_http11/puma_http11.c +122 -118
- data/lib/puma/app/status.rb +10 -2
- data/lib/puma/binder.rb +10 -8
- data/lib/puma/cli.rb +3 -5
- data/lib/puma/client.rb +122 -72
- data/lib/puma/cluster/worker.rb +17 -17
- data/lib/puma/cluster/worker_handle.rb +38 -7
- data/lib/puma/cluster.rb +43 -29
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +104 -51
- data/lib/puma/const.rb +11 -11
- data/lib/puma/control_cli.rb +6 -2
- data/lib/puma/detect.rb +2 -0
- data/lib/puma/dsl.rb +151 -100
- data/lib/puma/error_logger.rb +3 -1
- data/lib/puma/events.rb +25 -10
- data/lib/puma/io_buffer.rb +8 -4
- data/lib/puma/launcher/bundle_pruner.rb +1 -1
- data/lib/puma/launcher.rb +54 -49
- data/lib/puma/minissl.rb +0 -1
- data/lib/puma/plugin/systemd.rb +3 -3
- data/lib/puma/rack/urlmap.rb +1 -1
- data/lib/puma/reactor.rb +19 -13
- data/lib/puma/request.rb +54 -39
- data/lib/puma/runner.rb +9 -18
- data/lib/puma/server.rb +114 -64
- data/lib/puma/single.rb +7 -4
- data/lib/puma/state_file.rb +3 -2
- data/lib/puma/thread_pool.rb +47 -82
- data/lib/puma/util.rb +0 -7
- data/lib/puma.rb +10 -0
- data/lib/rack/handler/puma.rb +2 -2
- data/tools/Dockerfile +15 -5
- metadata +7 -6
- data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/client.rb
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class IO
|
|
4
|
-
# We need to use this for a jruby work around on both 1.8 and 1.9.
|
|
5
|
-
# So this either creates the constant (on 1.8), or harmlessly
|
|
6
|
-
# reopens it (on 1.9).
|
|
7
|
-
module WaitReadable
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
|
|
11
3
|
require_relative 'detect'
|
|
12
4
|
require_relative 'io_buffer'
|
|
13
5
|
require 'tempfile'
|
|
@@ -64,6 +56,11 @@ module Puma
|
|
|
64
56
|
|
|
65
57
|
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
|
66
58
|
|
|
59
|
+
# See:
|
|
60
|
+
# https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
|
|
61
|
+
# https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
|
|
62
|
+
STRIP_OWS = /\A[ \t]+|[ \t]+\z/
|
|
63
|
+
|
|
67
64
|
# The object used for a request with no body. All requests with
|
|
68
65
|
# no body share this one object since it has no state.
|
|
69
66
|
EmptyBody = NullIO.new
|
|
@@ -111,7 +108,8 @@ module Puma
|
|
|
111
108
|
end
|
|
112
109
|
|
|
113
110
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
114
|
-
:tempfile, :io_buffer, :http_content_length_limit_exceeded
|
|
111
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded,
|
|
112
|
+
:requests_served
|
|
115
113
|
|
|
116
114
|
attr_writer :peerip, :http_content_length_limit
|
|
117
115
|
|
|
@@ -133,9 +131,9 @@ module Puma
|
|
|
133
131
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
|
134
132
|
end
|
|
135
133
|
|
|
136
|
-
# For the hijack protocol
|
|
137
|
-
#
|
|
138
|
-
def
|
|
134
|
+
# For the full hijack protocol, `env['rack.hijack']` is set to
|
|
135
|
+
# `client.method :full_hijack`
|
|
136
|
+
def full_hijack
|
|
139
137
|
@hijacked = true
|
|
140
138
|
env[HIJACK_IO] ||= @io
|
|
141
139
|
end
|
|
@@ -150,15 +148,16 @@ module Puma
|
|
|
150
148
|
end
|
|
151
149
|
|
|
152
150
|
# Number of seconds until the timeout elapses.
|
|
151
|
+
# @!attribute [r] timeout
|
|
153
152
|
def timeout
|
|
154
153
|
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
155
154
|
end
|
|
156
155
|
|
|
157
|
-
def reset
|
|
156
|
+
def reset
|
|
158
157
|
@parser.reset
|
|
159
158
|
@io_buffer.reset
|
|
160
159
|
@read_header = true
|
|
161
|
-
@read_proxy = !!@expect_proxy_proto
|
|
160
|
+
@read_proxy = !!@expect_proxy_proto && @requests_served.zero?
|
|
162
161
|
@env = @proto_env.dup
|
|
163
162
|
@parsed_bytes = 0
|
|
164
163
|
@ready = false
|
|
@@ -166,11 +165,14 @@ module Puma
|
|
|
166
165
|
@peerip = nil if @remote_addr_header
|
|
167
166
|
@in_last_chunk = false
|
|
168
167
|
@http_content_length_limit_exceeded = false
|
|
168
|
+
end
|
|
169
169
|
|
|
170
|
+
# only used with back-to-back requests contained in the buffer
|
|
171
|
+
def process_back_to_back_requests
|
|
170
172
|
if @buffer
|
|
171
173
|
return false unless try_to_parse_proxy_protocol
|
|
172
174
|
|
|
173
|
-
@parsed_bytes =
|
|
175
|
+
@parsed_bytes = parser_execute
|
|
174
176
|
|
|
175
177
|
if @parser.finished?
|
|
176
178
|
return setup_body
|
|
@@ -178,25 +180,20 @@ module Puma
|
|
|
178
180
|
raise HttpParserError,
|
|
179
181
|
"HEADER is longer than allowed, aborting client early."
|
|
180
182
|
end
|
|
181
|
-
|
|
182
|
-
return false
|
|
183
|
-
else
|
|
184
|
-
begin
|
|
185
|
-
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
|
186
|
-
return try_to_finish
|
|
187
|
-
end
|
|
188
|
-
rescue IOError
|
|
189
|
-
# swallow it
|
|
190
|
-
end
|
|
191
183
|
end
|
|
192
184
|
end
|
|
193
185
|
|
|
186
|
+
# if a client sends back-to-back requests, the buffer may contain one or more
|
|
187
|
+
# of them.
|
|
188
|
+
def has_back_to_back_requests?
|
|
189
|
+
!(@buffer.nil? || @buffer.empty?)
|
|
190
|
+
end
|
|
191
|
+
|
|
194
192
|
def close
|
|
195
193
|
tempfile_close
|
|
196
194
|
begin
|
|
197
195
|
@io.close
|
|
198
196
|
rescue IOError, Errno::EBADF
|
|
199
|
-
Puma::Util.purge_interrupt_queue
|
|
200
197
|
end
|
|
201
198
|
end
|
|
202
199
|
|
|
@@ -214,20 +211,36 @@ module Puma
|
|
|
214
211
|
def try_to_parse_proxy_protocol
|
|
215
212
|
if @read_proxy
|
|
216
213
|
if @expect_proxy_proto == :v1
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
214
|
+
crlf_index = @buffer.index "\r\n"
|
|
215
|
+
|
|
216
|
+
unless crlf_index
|
|
217
|
+
if "PROXY ".start_with? @buffer
|
|
218
|
+
return false
|
|
219
|
+
elsif @buffer.start_with? "PROXY "
|
|
220
|
+
if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
|
|
221
|
+
raise ConnectionError, "PROXY protocol v1 line is too long"
|
|
221
222
|
end
|
|
222
|
-
|
|
223
|
+
return false
|
|
223
224
|
end
|
|
224
|
-
|
|
225
|
-
# request, this is just HTTP from a non-PROXY client; move on
|
|
225
|
+
|
|
226
226
|
@read_proxy = false
|
|
227
|
-
return
|
|
228
|
-
else
|
|
229
|
-
return false
|
|
227
|
+
return true
|
|
230
228
|
end
|
|
229
|
+
|
|
230
|
+
if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
|
|
231
|
+
raise ConnectionError, "PROXY protocol v1 line is too long"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
|
|
235
|
+
if md[1]
|
|
236
|
+
@peerip = md[1].split(" ")[0]
|
|
237
|
+
end
|
|
238
|
+
@buffer = md.post_match
|
|
239
|
+
end
|
|
240
|
+
# if the buffer has a \r\n but doesn't have a PROXY protocol
|
|
241
|
+
# request, this is just HTTP from a non-PROXY client; move on
|
|
242
|
+
@read_proxy = false
|
|
243
|
+
return @buffer.size > 0
|
|
231
244
|
end
|
|
232
245
|
end
|
|
233
246
|
true
|
|
@@ -273,26 +286,28 @@ module Puma
|
|
|
273
286
|
|
|
274
287
|
return false unless try_to_parse_proxy_protocol
|
|
275
288
|
|
|
276
|
-
@parsed_bytes =
|
|
289
|
+
@parsed_bytes = parser_execute
|
|
277
290
|
|
|
278
291
|
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
|
279
292
|
@http_content_length_limit_exceeded = true
|
|
280
293
|
end
|
|
281
294
|
|
|
282
295
|
if @parser.finished?
|
|
283
|
-
|
|
296
|
+
setup_body
|
|
284
297
|
elsif @parsed_bytes >= MAX_HEADER
|
|
285
298
|
raise HttpParserError,
|
|
286
299
|
"HEADER is longer than allowed, aborting client early."
|
|
300
|
+
else
|
|
301
|
+
false
|
|
287
302
|
end
|
|
288
|
-
|
|
289
|
-
false
|
|
290
303
|
end
|
|
291
304
|
|
|
292
305
|
def eagerly_finish
|
|
293
306
|
return true if @ready
|
|
294
|
-
|
|
295
|
-
|
|
307
|
+
while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
|
|
308
|
+
return true if try_to_finish
|
|
309
|
+
end
|
|
310
|
+
false
|
|
296
311
|
end
|
|
297
312
|
|
|
298
313
|
def finish(timeout)
|
|
@@ -300,6 +315,44 @@ module Puma
|
|
|
300
315
|
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
|
301
316
|
end
|
|
302
317
|
|
|
318
|
+
# Wraps `@parser.execute` and adds meaningful error messages
|
|
319
|
+
# @return [Integer] bytes of buffer read by parser
|
|
320
|
+
#
|
|
321
|
+
def parser_execute
|
|
322
|
+
@parser.execute(@env, @buffer, @parsed_bytes)
|
|
323
|
+
rescue => e
|
|
324
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
325
|
+
raise e unless HttpParserError === e && e.message.include?('non-SSL')
|
|
326
|
+
|
|
327
|
+
req, _ = @buffer.split "\r\n\r\n"
|
|
328
|
+
request_line, headers = req.split "\r\n", 2
|
|
329
|
+
|
|
330
|
+
# below checks for request issues and changes error message accordingly
|
|
331
|
+
if !@env.key? REQUEST_METHOD
|
|
332
|
+
if request_line.count(' ') != 2
|
|
333
|
+
# maybe this is an SSL connection ?
|
|
334
|
+
raise e
|
|
335
|
+
else
|
|
336
|
+
method = request_line[/\A[^ ]+/]
|
|
337
|
+
raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
|
|
338
|
+
end
|
|
339
|
+
elsif !@env.key? REQUEST_PATH
|
|
340
|
+
path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
|
|
341
|
+
raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
|
|
342
|
+
elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
|
|
343
|
+
query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
|
|
344
|
+
raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
|
|
345
|
+
elsif !@env.key? SERVER_PROTOCOL
|
|
346
|
+
# protocol is bad
|
|
347
|
+
text = request_line[/[^ ]*\z/]
|
|
348
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
|
|
349
|
+
elsif !headers.empty?
|
|
350
|
+
# headers are bad
|
|
351
|
+
hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
|
|
352
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
303
356
|
def timeout!
|
|
304
357
|
write_error(408) if in_data_phase
|
|
305
358
|
raise ConnectionError
|
|
@@ -374,17 +427,18 @@ module Puma
|
|
|
374
427
|
if te
|
|
375
428
|
te_lwr = te.downcase
|
|
376
429
|
if te.include? ','
|
|
377
|
-
te_ary = te_lwr.split
|
|
430
|
+
te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
|
|
378
431
|
te_count = te_ary.count CHUNKED
|
|
379
432
|
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
|
380
|
-
if
|
|
381
|
-
@env.delete TRANSFER_ENCODING2
|
|
382
|
-
return setup_chunked_body body
|
|
383
|
-
elsif te_count >= 1
|
|
433
|
+
if te_count > 1
|
|
384
434
|
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
|
435
|
+
elsif te_ary.last != CHUNKED
|
|
436
|
+
raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
|
|
385
437
|
elsif !te_valid
|
|
386
438
|
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
|
387
439
|
end
|
|
440
|
+
@env.delete TRANSFER_ENCODING2
|
|
441
|
+
return setup_chunked_body body
|
|
388
442
|
elsif te_lwr == CHUNKED
|
|
389
443
|
@env.delete TRANSFER_ENCODING2
|
|
390
444
|
return setup_chunked_body body
|
|
@@ -462,40 +516,36 @@ module Puma
|
|
|
462
516
|
# after this
|
|
463
517
|
remain = @body_remain
|
|
464
518
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
519
|
+
# don't bother with reading zero bytes
|
|
520
|
+
unless remain.zero?
|
|
521
|
+
begin
|
|
522
|
+
chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
|
|
523
|
+
rescue IO::WaitReadable
|
|
524
|
+
return false
|
|
525
|
+
rescue SystemCallError, IOError
|
|
526
|
+
raise ConnectionError, "Connection error detected during read"
|
|
527
|
+
end
|
|
470
528
|
|
|
471
|
-
|
|
472
|
-
chunk
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
529
|
+
# No chunk means a closed socket
|
|
530
|
+
unless chunk
|
|
531
|
+
@body.close
|
|
532
|
+
@buffer = nil
|
|
533
|
+
set_ready
|
|
534
|
+
raise EOFError
|
|
535
|
+
end
|
|
478
536
|
|
|
479
|
-
|
|
480
|
-
unless chunk
|
|
481
|
-
@body.close
|
|
482
|
-
@buffer = nil
|
|
483
|
-
set_ready
|
|
484
|
-
raise EOFError
|
|
537
|
+
remain -= @body.write(chunk)
|
|
485
538
|
end
|
|
486
539
|
|
|
487
|
-
remain -= @body.write(chunk)
|
|
488
|
-
|
|
489
540
|
if remain <= 0
|
|
490
541
|
@body.rewind
|
|
491
542
|
@buffer = nil
|
|
492
543
|
set_ready
|
|
493
|
-
|
|
544
|
+
true
|
|
545
|
+
else
|
|
546
|
+
@body_remain = remain
|
|
547
|
+
false
|
|
494
548
|
end
|
|
495
|
-
|
|
496
|
-
@body_remain = remain
|
|
497
|
-
|
|
498
|
-
false
|
|
499
549
|
end
|
|
500
550
|
|
|
501
551
|
def read_chunked_body
|
data/lib/puma/cluster/worker.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Puma
|
|
|
14
14
|
class Worker < Puma::Runner # :nodoc:
|
|
15
15
|
attr_reader :index, :master
|
|
16
16
|
|
|
17
|
-
def initialize(index:, master:, launcher:, pipes:,
|
|
17
|
+
def initialize(index:, master:, launcher:, pipes:, app: nil)
|
|
18
18
|
super(launcher)
|
|
19
19
|
|
|
20
20
|
@index = index
|
|
@@ -23,7 +23,8 @@ module Puma
|
|
|
23
23
|
@worker_write = pipes[:worker_write]
|
|
24
24
|
@fork_pipe = pipes[:fork_pipe]
|
|
25
25
|
@wakeup = pipes[:wakeup]
|
|
26
|
-
@
|
|
26
|
+
@app = app
|
|
27
|
+
@server = nil
|
|
27
28
|
@hook_data = {}
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -57,7 +58,7 @@ module Puma
|
|
|
57
58
|
@config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
|
|
58
59
|
|
|
59
60
|
begin
|
|
60
|
-
|
|
61
|
+
@server = start_server
|
|
61
62
|
rescue Exception => e
|
|
62
63
|
log "! Unable to start worker"
|
|
63
64
|
log e
|
|
@@ -85,7 +86,7 @@ module Puma
|
|
|
85
86
|
if idx == -1 # stop server
|
|
86
87
|
if restart_server.length > 0
|
|
87
88
|
restart_server.clear
|
|
88
|
-
server.begin_restart(true)
|
|
89
|
+
@server.begin_restart(true)
|
|
89
90
|
@config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
|
|
90
91
|
end
|
|
91
92
|
elsif idx == -2 # refork cycle is done
|
|
@@ -103,20 +104,19 @@ module Puma
|
|
|
103
104
|
Signal.trap "SIGTERM" do
|
|
104
105
|
@worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
|
|
105
106
|
restart_server.clear
|
|
106
|
-
server.stop
|
|
107
|
+
@server.stop
|
|
107
108
|
restart_server << false
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
begin
|
|
111
112
|
@worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
|
|
112
113
|
rescue SystemCallError, IOError
|
|
113
|
-
Puma::Util.purge_interrupt_queue
|
|
114
114
|
STDERR.puts "Master seems to have exited, exiting."
|
|
115
115
|
return
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
while restart_server.pop
|
|
119
|
-
server_thread = server.run
|
|
119
|
+
server_thread = @server.run
|
|
120
120
|
|
|
121
121
|
if @log_writer.debug? && index == 0
|
|
122
122
|
debug_loaded_extensions "Loaded Extensions - worker 0:"
|
|
@@ -128,16 +128,16 @@ module Puma
|
|
|
128
128
|
|
|
129
129
|
while true
|
|
130
130
|
begin
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
io << payload
|
|
131
|
+
payload = base_payload.dup
|
|
132
|
+
|
|
133
|
+
hsh = @server.stats
|
|
134
|
+
hsh.each do |k, v|
|
|
135
|
+
payload << %Q! "#{k}":#{v || 0},!
|
|
136
|
+
end
|
|
137
|
+
# sub call properly adds 'closing' string
|
|
138
|
+
io << payload.sub(/,\z/, " }\n")
|
|
139
|
+
@server.reset_max
|
|
139
140
|
rescue IOError
|
|
140
|
-
Puma::Util.purge_interrupt_queue
|
|
141
141
|
break
|
|
142
142
|
end
|
|
143
143
|
sleep @options[:worker_check_interval]
|
|
@@ -165,7 +165,7 @@ module Puma
|
|
|
165
165
|
launcher: @launcher,
|
|
166
166
|
pipes: { check_pipe: @check_pipe,
|
|
167
167
|
worker_write: @worker_write },
|
|
168
|
-
|
|
168
|
+
app: @app
|
|
169
169
|
new_worker.run
|
|
170
170
|
end
|
|
171
171
|
|
|
@@ -4,13 +4,15 @@ module Puma
|
|
|
4
4
|
class Cluster < Runner
|
|
5
5
|
#—————————————————————— DO NOT USE — this class is for internal use only ———
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
# This class represents a worker process from the perspective of the puma
|
|
9
8
|
# master process. It contains information about the process and its health
|
|
10
9
|
# and it exposes methods to control the process via IPC. It does not
|
|
11
10
|
# include the actual logic executed by the worker process itself. For that,
|
|
12
11
|
# see Puma::Cluster::Worker.
|
|
13
12
|
class WorkerHandle # :nodoc:
|
|
13
|
+
# array of stat 'max' keys
|
|
14
|
+
WORKER_MAX_KEYS = [:backlog_max, :reactor_max]
|
|
15
|
+
|
|
14
16
|
def initialize(idx, pid, phase, options)
|
|
15
17
|
@index = idx
|
|
16
18
|
@pid = pid
|
|
@@ -23,12 +25,13 @@ module Puma
|
|
|
23
25
|
@last_checkin = Time.now
|
|
24
26
|
@last_status = {}
|
|
25
27
|
@term = false
|
|
28
|
+
@worker_max = Array.new WORKER_MAX_KEYS.length, 0
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
|
31
|
+
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at, :process_status
|
|
29
32
|
|
|
30
33
|
# @version 5.0.0
|
|
31
|
-
attr_writer :pid, :phase
|
|
34
|
+
attr_writer :pid, :phase, :process_status
|
|
32
35
|
|
|
33
36
|
def booted?
|
|
34
37
|
@stage == :booted
|
|
@@ -51,12 +54,40 @@ module Puma
|
|
|
51
54
|
@term
|
|
52
55
|
end
|
|
53
56
|
|
|
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
|
-
|
|
57
57
|
def ping!(status)
|
|
58
|
+
hsh = {}
|
|
59
|
+
k, v = nil, nil
|
|
60
|
+
status.tr('}{"', '').strip.split(", ") do |kv|
|
|
61
|
+
cntr = 0
|
|
62
|
+
kv.split(':') do |t|
|
|
63
|
+
if cntr == 0
|
|
64
|
+
k = t
|
|
65
|
+
cntr = 1
|
|
66
|
+
else
|
|
67
|
+
v = t
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
hsh[k.to_sym] = v.to_i
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# check stat max values, we can't signal workers to reset the max values,
|
|
74
|
+
# so we do so here
|
|
75
|
+
WORKER_MAX_KEYS.each_with_index do |key, idx|
|
|
76
|
+
next unless hsh[key]
|
|
77
|
+
|
|
78
|
+
if hsh[key] < @worker_max[idx]
|
|
79
|
+
hsh[key] = @worker_max[idx]
|
|
80
|
+
else
|
|
81
|
+
@worker_max[idx] = hsh[key]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
58
84
|
@last_checkin = Time.now
|
|
59
|
-
@last_status =
|
|
85
|
+
@last_status = hsh
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Resets max values to zero. Called whenever `Cluster#stats` is called
|
|
89
|
+
def reset_max
|
|
90
|
+
WORKER_MAX_KEYS.length.times { |idx| @worker_max[idx] = 0 }
|
|
60
91
|
end
|
|
61
92
|
|
|
62
93
|
# @see Puma::Cluster#check_workers
|
data/lib/puma/cluster.rb
CHANGED
|
@@ -22,7 +22,8 @@ module Puma
|
|
|
22
22
|
@workers = []
|
|
23
23
|
@next_check = Time.now
|
|
24
24
|
|
|
25
|
-
@
|
|
25
|
+
@worker_max = [] # keeps track of 'max' stat values
|
|
26
|
+
@pending_phased_restart = false
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
# Returns the list of cluster worker handles.
|
|
@@ -44,10 +45,14 @@ module Puma
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def start_phased_restart
|
|
48
|
-
@events.
|
|
48
|
+
def start_phased_restart(refork = false)
|
|
49
|
+
@events.fire_before_restart!
|
|
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.
|
|
@@ -166,7 +171,7 @@ module Puma
|
|
|
166
171
|
(@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
|
|
167
172
|
end
|
|
168
173
|
|
|
169
|
-
def check_workers
|
|
174
|
+
def check_workers(refork = false)
|
|
170
175
|
return if @next_check >= Time.now
|
|
171
176
|
|
|
172
177
|
@next_check = Time.now + @options[:worker_check_interval]
|
|
@@ -181,10 +186,15 @@ module Puma
|
|
|
181
186
|
# we need to phase any workers out (which will restart
|
|
182
187
|
# in the right phase).
|
|
183
188
|
#
|
|
184
|
-
w = @workers.find { |x| x.phase
|
|
189
|
+
w = @workers.find { |x| x.phase < @phase }
|
|
185
190
|
|
|
186
191
|
if w
|
|
187
|
-
|
|
192
|
+
if refork
|
|
193
|
+
log "- Stopping #{w.pid} for refork..."
|
|
194
|
+
else
|
|
195
|
+
log "- Stopping #{w.pid} for phased upgrade..."
|
|
196
|
+
end
|
|
197
|
+
|
|
188
198
|
unless w.term?
|
|
189
199
|
w.term
|
|
190
200
|
log "- #{w.signal} sent to #{w.pid}..."
|
|
@@ -211,12 +221,11 @@ module Puma
|
|
|
211
221
|
pipes[:wakeup] = @wakeup
|
|
212
222
|
end
|
|
213
223
|
|
|
214
|
-
server = start_server if preload?
|
|
215
224
|
new_worker = Worker.new index: index,
|
|
216
225
|
master: master,
|
|
217
226
|
launcher: @launcher,
|
|
218
227
|
pipes: pipes,
|
|
219
|
-
|
|
228
|
+
app: (app if preload?)
|
|
220
229
|
new_worker.run
|
|
221
230
|
end
|
|
222
231
|
|
|
@@ -228,7 +237,7 @@ module Puma
|
|
|
228
237
|
def phased_restart(refork = false)
|
|
229
238
|
return false if @options[:preload_app] && !refork
|
|
230
239
|
|
|
231
|
-
@
|
|
240
|
+
@pending_phased_restart = refork ? :refork : true
|
|
232
241
|
wakeup!
|
|
233
242
|
|
|
234
243
|
true
|
|
@@ -258,11 +267,14 @@ module Puma
|
|
|
258
267
|
end
|
|
259
268
|
|
|
260
269
|
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
|
261
|
-
# the master process.
|
|
270
|
+
# the master process. Calling this also resets stat 'max' values to zero.
|
|
262
271
|
# @!attribute [r] stats
|
|
272
|
+
# @return [Hash]
|
|
273
|
+
|
|
263
274
|
def stats
|
|
264
275
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
|
265
276
|
worker_status = @workers.map do |w|
|
|
277
|
+
w.reset_max
|
|
266
278
|
{
|
|
267
279
|
started_at: utc_iso8601(w.started_at),
|
|
268
280
|
pid: w.pid,
|
|
@@ -273,7 +285,6 @@ module Puma
|
|
|
273
285
|
last_status: w.last_status,
|
|
274
286
|
}
|
|
275
287
|
end
|
|
276
|
-
|
|
277
288
|
{
|
|
278
289
|
started_at: utc_iso8601(@started_at),
|
|
279
290
|
workers: @workers.size,
|
|
@@ -342,7 +353,7 @@ module Puma
|
|
|
342
353
|
|
|
343
354
|
stop_workers
|
|
344
355
|
stop
|
|
345
|
-
@events.
|
|
356
|
+
@events.fire_after_stopped!
|
|
346
357
|
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
|
347
358
|
exit 0 # Clean exit, workers were stopped
|
|
348
359
|
end
|
|
@@ -359,16 +370,12 @@ module Puma
|
|
|
359
370
|
|
|
360
371
|
if preload?
|
|
361
372
|
# Threads explicitly marked as fork safe will be ignored. Used in Rails,
|
|
362
|
-
# but may be used by anyone.
|
|
363
|
-
|
|
364
|
-
# where calling thread_variable_get on a Process::Waiter will segfault.
|
|
365
|
-
# We can drop that clause once those versions of Ruby are no longer
|
|
366
|
-
# supported.
|
|
367
|
-
fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
|
|
373
|
+
# but may be used by anyone.
|
|
374
|
+
fork_safe = ->(t) { t.thread_variable_get(:fork_safe) }
|
|
368
375
|
|
|
369
376
|
before = Thread.list.reject(&fork_safe)
|
|
370
377
|
|
|
371
|
-
log "* Restarts: (\u2714) hot (\u2716) phased"
|
|
378
|
+
log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
372
379
|
log "* Preloading application"
|
|
373
380
|
load_and_bind
|
|
374
381
|
|
|
@@ -386,7 +393,7 @@ module Puma
|
|
|
386
393
|
end
|
|
387
394
|
end
|
|
388
395
|
else
|
|
389
|
-
log "* Restarts: (\u2714) hot (\u2714) phased"
|
|
396
|
+
log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
390
397
|
|
|
391
398
|
unless @config.app_configured?
|
|
392
399
|
error "No application configured, nothing to run"
|
|
@@ -413,6 +420,7 @@ module Puma
|
|
|
413
420
|
|
|
414
421
|
log "Use Ctrl-C to stop"
|
|
415
422
|
|
|
423
|
+
warn_ruby_mn_threads
|
|
416
424
|
single_worker_warning
|
|
417
425
|
|
|
418
426
|
redirect_io
|
|
@@ -447,14 +455,18 @@ module Puma
|
|
|
447
455
|
break
|
|
448
456
|
end
|
|
449
457
|
|
|
450
|
-
if @
|
|
451
|
-
start_phased_restart
|
|
452
|
-
|
|
453
|
-
in_phased_restart =
|
|
458
|
+
if @pending_phased_restart
|
|
459
|
+
start_phased_restart(@pending_phased_restart == :refork)
|
|
460
|
+
|
|
461
|
+
in_phased_restart = @pending_phased_restart
|
|
462
|
+
@pending_phased_restart = false
|
|
463
|
+
|
|
454
464
|
workers_not_booted = @options[:workers]
|
|
465
|
+
# worker 0 is not restarted on refork
|
|
466
|
+
workers_not_booted -= 1 if in_phased_restart == :refork
|
|
455
467
|
end
|
|
456
468
|
|
|
457
|
-
check_workers
|
|
469
|
+
check_workers(in_phased_restart == :refork)
|
|
458
470
|
|
|
459
471
|
if read.wait_readable([0, @next_check - Time.now].max)
|
|
460
472
|
req = read.read_nonblock(1)
|
|
@@ -497,7 +509,7 @@ module Puma
|
|
|
497
509
|
end
|
|
498
510
|
|
|
499
511
|
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
|
500
|
-
@events.
|
|
512
|
+
@events.fire_after_booted!
|
|
501
513
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
502
514
|
booted = true
|
|
503
515
|
end
|
|
@@ -514,7 +526,7 @@ module Puma
|
|
|
514
526
|
end
|
|
515
527
|
|
|
516
528
|
if in_phased_restart && workers_not_booted.zero?
|
|
517
|
-
@events.
|
|
529
|
+
@events.fire_after_booted!
|
|
518
530
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
519
531
|
in_phased_restart = false
|
|
520
532
|
end
|
|
@@ -570,7 +582,9 @@ module Puma
|
|
|
570
582
|
# `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
|
|
571
583
|
# 2. When `fork_worker` is enabled, some worker may not be direct children,
|
|
572
584
|
# but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
|
|
573
|
-
if reaped_children.delete(w.pid) || Process.
|
|
585
|
+
if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
|
|
586
|
+
w.process_status = status
|
|
587
|
+
@config.run_hooks(:after_worker_shutdown, w, @log_writer)
|
|
574
588
|
true
|
|
575
589
|
else
|
|
576
590
|
w.term if w.term?
|