puma 6.0.0 → 6.6.0
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 +392 -13
- data/LICENSE +0 -0
- data/README.md +135 -29
- data/bin/puma-wild +0 -0
- data/docs/architecture.md +0 -0
- data/docs/compile_options.md +0 -0
- data/docs/deployment.md +0 -0
- data/docs/fork_worker.md +11 -1
- 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/java_options.md +54 -0
- data/docs/jungle/README.md +0 -0
- data/docs/jungle/rc.d/README.md +0 -0
- data/docs/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +12 -0
- data/docs/nginx.md +1 -1
- data/docs/plugins.md +4 -0
- data/docs/rails_dev_mode.md +0 -0
- data/docs/restart.md +1 -0
- data/docs/signals.md +2 -2
- data/docs/stats.md +8 -3
- data/docs/systemd.md +13 -7
- data/docs/testing_benchmarks_local_files.md +0 -0
- data/docs/testing_test_rackup_ci_files.md +0 -0
- data/ext/puma_http11/PumaHttp11Service.java +0 -0
- data/ext/puma_http11/ext_help.h +0 -0
- data/ext/puma_http11/extconf.rb +21 -14
- data/ext/puma_http11/http11_parser.c +0 -0
- data/ext/puma_http11/http11_parser.h +0 -0
- data/ext/puma_http11/http11_parser.java.rl +0 -0
- data/ext/puma_http11/http11_parser.rl +0 -0
- data/ext/puma_http11/http11_parser_common.rl +0 -0
- data/ext/puma_http11/mini_ssl.c +107 -10
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +30 -7
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +0 -0
- 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 +26 -15
- data/lib/puma/cli.rb +13 -5
- data/lib/puma/client.rb +113 -26
- data/lib/puma/cluster/worker.rb +14 -6
- data/lib/puma/cluster/worker_handle.rb +4 -5
- data/lib/puma/cluster.rb +93 -22
- data/lib/puma/commonlogger.rb +21 -14
- data/lib/puma/configuration.rb +42 -22
- data/lib/puma/const.rb +149 -89
- data/lib/puma/control_cli.rb +16 -9
- data/lib/puma/detect.rb +5 -4
- data/lib/puma/dsl.rb +432 -40
- data/lib/puma/error_logger.rb +6 -5
- data/lib/puma/events.rb +0 -0
- data/lib/puma/io_buffer.rb +10 -0
- data/lib/puma/jruby_restart.rb +0 -16
- data/lib/puma/json_serialization.rb +0 -0
- data/lib/puma/launcher/bundle_pruner.rb +0 -0
- data/lib/puma/launcher.rb +29 -29
- data/lib/puma/log_writer.rb +23 -13
- data/lib/puma/minissl/context_builder.rb +4 -0
- data/lib/puma/minissl.rb +23 -0
- data/lib/puma/null_io.rb +42 -2
- data/lib/puma/plugin/systemd.rb +90 -0
- data/lib/puma/plugin/tmp_restart.rb +0 -0
- data/lib/puma/plugin.rb +0 -0
- data/lib/puma/rack/builder.rb +2 -2
- data/lib/puma/rack/urlmap.rb +1 -1
- data/lib/puma/rack_default.rb +18 -3
- data/lib/puma/reactor.rb +17 -8
- data/lib/puma/request.rb +207 -126
- data/lib/puma/runner.rb +26 -4
- data/lib/puma/sd_notify.rb +146 -0
- data/lib/puma/server.rb +121 -49
- data/lib/puma/single.rb +3 -1
- data/lib/puma/state_file.rb +2 -2
- data/lib/puma/thread_pool.rb +56 -9
- data/lib/puma/util.rb +1 -1
- data/lib/puma.rb +1 -3
- data/lib/rack/handler/puma.rb +116 -86
- data/tools/Dockerfile +2 -2
- data/tools/trickletest.rb +0 -0
- metadata +12 -13
- data/lib/puma/systemd.rb +0 -47
data/lib/puma/client.rb
CHANGED
@@ -9,8 +9,8 @@ class IO
|
|
9
9
|
end
|
10
10
|
|
11
11
|
require_relative 'detect'
|
12
|
+
require_relative 'io_buffer'
|
12
13
|
require 'tempfile'
|
13
|
-
require 'forwardable'
|
14
14
|
|
15
15
|
if Puma::IS_JRUBY
|
16
16
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
@@ -48,7 +48,16 @@ module Puma
|
|
48
48
|
|
49
49
|
# chunked body validation
|
50
50
|
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
51
|
-
CHUNK_VALID_ENDING =
|
51
|
+
CHUNK_VALID_ENDING = Const::LINE_END
|
52
|
+
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
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
|
52
61
|
|
53
62
|
# Content-Length header value validation
|
54
63
|
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
@@ -60,13 +69,13 @@ module Puma
|
|
60
69
|
EmptyBody = NullIO.new
|
61
70
|
|
62
71
|
include Puma::Const
|
63
|
-
extend Forwardable
|
64
72
|
|
65
73
|
def initialize(io, env=nil)
|
66
74
|
@io = io
|
67
75
|
@to_io = io.to_io
|
76
|
+
@io_buffer = IOBuffer.new
|
68
77
|
@proto_env = env
|
69
|
-
@env = env
|
78
|
+
@env = env&.dup
|
70
79
|
|
71
80
|
@parser = HttpParser.new
|
72
81
|
@parsed_bytes = 0
|
@@ -84,6 +93,9 @@ module Puma
|
|
84
93
|
@requests_served = 0
|
85
94
|
@hijacked = false
|
86
95
|
|
96
|
+
@http_content_length_limit = nil
|
97
|
+
@http_content_length_limit_exceeded = false
|
98
|
+
|
87
99
|
@peerip = nil
|
88
100
|
@peer_family = nil
|
89
101
|
@listener = nil
|
@@ -93,16 +105,22 @@ module Puma
|
|
93
105
|
@body_remain = 0
|
94
106
|
|
95
107
|
@in_last_chunk = false
|
108
|
+
|
109
|
+
# need unfrozen ASCII-8BIT, +'' is UTF-8
|
110
|
+
@read_buffer = String.new # rubocop: disable Performance/UnfreezeString
|
96
111
|
end
|
97
112
|
|
98
113
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
99
|
-
:tempfile
|
114
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded
|
100
115
|
|
101
|
-
attr_writer :peerip
|
116
|
+
attr_writer :peerip, :http_content_length_limit
|
102
117
|
|
103
118
|
attr_accessor :remote_addr_header, :listener
|
104
119
|
|
105
|
-
|
120
|
+
# Remove in Puma 7?
|
121
|
+
def closed?
|
122
|
+
@to_io.closed?
|
123
|
+
end
|
106
124
|
|
107
125
|
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
108
126
|
# used for MiniSSL::Socket
|
@@ -138,16 +156,16 @@ module Puma
|
|
138
156
|
|
139
157
|
def reset(fast_check=true)
|
140
158
|
@parser.reset
|
159
|
+
@io_buffer.reset
|
141
160
|
@read_header = true
|
142
161
|
@read_proxy = !!@expect_proxy_proto
|
143
162
|
@env = @proto_env.dup
|
144
|
-
@body = nil
|
145
|
-
@tempfile = nil
|
146
163
|
@parsed_bytes = 0
|
147
164
|
@ready = false
|
148
165
|
@body_remain = 0
|
149
166
|
@peerip = nil if @remote_addr_header
|
150
167
|
@in_last_chunk = false
|
168
|
+
@http_content_length_limit_exceeded = false
|
151
169
|
|
152
170
|
if @buffer
|
153
171
|
return false unless try_to_parse_proxy_protocol
|
@@ -170,11 +188,11 @@ module Puma
|
|
170
188
|
rescue IOError
|
171
189
|
# swallow it
|
172
190
|
end
|
173
|
-
|
174
191
|
end
|
175
192
|
end
|
176
193
|
|
177
194
|
def close
|
195
|
+
tempfile_close
|
178
196
|
begin
|
179
197
|
@io.close
|
180
198
|
rescue IOError, Errno::EBADF
|
@@ -182,6 +200,15 @@ module Puma
|
|
182
200
|
end
|
183
201
|
end
|
184
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
|
+
|
185
212
|
# If necessary, read the PROXY protocol from the buffer. Returns
|
186
213
|
# false if more data is needed.
|
187
214
|
def try_to_parse_proxy_protocol
|
@@ -207,8 +234,20 @@ module Puma
|
|
207
234
|
end
|
208
235
|
|
209
236
|
def try_to_finish
|
237
|
+
if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
|
238
|
+
@http_content_length_limit_exceeded = true
|
239
|
+
end
|
240
|
+
|
241
|
+
if @http_content_length_limit_exceeded
|
242
|
+
@buffer = nil
|
243
|
+
@body = EmptyBody
|
244
|
+
set_ready
|
245
|
+
return true
|
246
|
+
end
|
247
|
+
|
210
248
|
return read_body if in_data_phase
|
211
249
|
|
250
|
+
data = nil
|
212
251
|
begin
|
213
252
|
data = @io.read_nonblock(CHUNK_SIZE)
|
214
253
|
rescue IO::WaitReadable
|
@@ -236,6 +275,10 @@ module Puma
|
|
236
275
|
|
237
276
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
238
277
|
|
278
|
+
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
279
|
+
@http_content_length_limit_exceeded = true
|
280
|
+
end
|
281
|
+
|
239
282
|
if @parser.finished?
|
240
283
|
return setup_body
|
241
284
|
elsif @parsed_bytes >= MAX_HEADER
|
@@ -357,8 +400,8 @@ module Puma
|
|
357
400
|
cl = @env[CONTENT_LENGTH]
|
358
401
|
|
359
402
|
if cl
|
360
|
-
# cannot contain characters that are not \d
|
361
|
-
if CONTENT_LENGTH_VALUE_INVALID.match? cl
|
403
|
+
# cannot contain characters that are not \d, or be empty
|
404
|
+
if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
|
362
405
|
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
363
406
|
end
|
364
407
|
else
|
@@ -368,18 +411,33 @@ module Puma
|
|
368
411
|
return true
|
369
412
|
end
|
370
413
|
|
371
|
-
|
414
|
+
content_length = cl.to_i
|
415
|
+
|
416
|
+
remain = content_length - body.bytesize
|
372
417
|
|
373
418
|
if remain <= 0
|
374
|
-
|
375
|
-
|
419
|
+
# Part of the body is a pipelined request OR garbage. We'll deal with that later.
|
420
|
+
if content_length == 0
|
421
|
+
@body = EmptyBody
|
422
|
+
if body.empty?
|
423
|
+
@buffer = nil
|
424
|
+
else
|
425
|
+
@buffer = body
|
426
|
+
end
|
427
|
+
elsif remain == 0
|
428
|
+
@body = StringIO.new body
|
429
|
+
@buffer = nil
|
430
|
+
else
|
431
|
+
@body = StringIO.new(body[0,content_length])
|
432
|
+
@buffer = body[content_length..-1]
|
433
|
+
end
|
376
434
|
set_ready
|
377
435
|
return true
|
378
436
|
end
|
379
437
|
|
380
438
|
if remain > MAX_BODY
|
381
|
-
@body = Tempfile.
|
382
|
-
@body.
|
439
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
440
|
+
File.unlink @body.path unless IS_WINDOWS
|
383
441
|
@body.binmode
|
384
442
|
@tempfile = @body
|
385
443
|
else
|
@@ -411,7 +469,7 @@ module Puma
|
|
411
469
|
end
|
412
470
|
|
413
471
|
begin
|
414
|
-
chunk = @io.read_nonblock(want)
|
472
|
+
chunk = @io.read_nonblock(want, @read_buffer)
|
415
473
|
rescue IO::WaitReadable
|
416
474
|
return false
|
417
475
|
rescue SystemCallError, IOError
|
@@ -443,7 +501,7 @@ module Puma
|
|
443
501
|
def read_chunked_body
|
444
502
|
while true
|
445
503
|
begin
|
446
|
-
chunk = @io.read_nonblock(
|
504
|
+
chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
|
447
505
|
rescue IO::WaitReadable
|
448
506
|
return false
|
449
507
|
rescue SystemCallError, IOError
|
@@ -469,9 +527,10 @@ module Puma
|
|
469
527
|
@chunked_body = true
|
470
528
|
@partial_part_left = 0
|
471
529
|
@prev_chunk = ""
|
530
|
+
@excess_cr = 0
|
472
531
|
|
473
|
-
@body = Tempfile.
|
474
|
-
@body.
|
532
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
533
|
+
File.unlink @body.path unless IS_WINDOWS
|
475
534
|
@body.binmode
|
476
535
|
@tempfile = @body
|
477
536
|
@chunked_content_length = 0
|
@@ -519,7 +578,7 @@ module Puma
|
|
519
578
|
|
520
579
|
while !io.eof?
|
521
580
|
line = io.gets
|
522
|
-
if line.end_with?(
|
581
|
+
if line.end_with?(CHUNK_VALID_ENDING)
|
523
582
|
# Puma doesn't process chunk extensions, but should parse if they're
|
524
583
|
# present, which is the reason for the semicolon regex
|
525
584
|
chunk_hex = line.strip[/\A[^;]+/]
|
@@ -531,19 +590,39 @@ module Puma
|
|
531
590
|
@in_last_chunk = true
|
532
591
|
@body.rewind
|
533
592
|
rest = io.read
|
534
|
-
|
535
|
-
if rest.bytesize < last_crlf_size
|
593
|
+
if rest.bytesize < CHUNK_VALID_ENDING_SIZE
|
536
594
|
@buffer = nil
|
537
|
-
@partial_part_left =
|
595
|
+
@partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
|
538
596
|
return false
|
539
597
|
else
|
540
|
-
|
598
|
+
# if the next character is a CRLF, set buffer to everything after that CRLF
|
599
|
+
start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
|
600
|
+
CHUNK_VALID_ENDING_SIZE
|
601
|
+
else # we have started a trailer section, which we do not support. skip it!
|
602
|
+
rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
|
603
|
+
end
|
604
|
+
|
605
|
+
@buffer = rest[start_of_rest..-1]
|
541
606
|
@buffer = nil if @buffer.empty?
|
542
607
|
set_ready
|
543
608
|
return true
|
544
609
|
end
|
545
610
|
end
|
546
611
|
|
612
|
+
# Track the excess as a function of the size of the
|
613
|
+
# header vs the size of the actual data. Excess can
|
614
|
+
# go negative (and is expected to) when the body is
|
615
|
+
# significant.
|
616
|
+
# The additional of chunk_hex.size and 2 compensates
|
617
|
+
# for a client sending 1 byte in a chunked body over
|
618
|
+
# a long period of time, making sure that that client
|
619
|
+
# isn't accidentally eventually punished.
|
620
|
+
@excess_cr += (line.size - len - chunk_hex.size - 2)
|
621
|
+
|
622
|
+
if @excess_cr >= MAX_CHUNK_EXCESS
|
623
|
+
raise HttpParserError, "Maximum chunk excess detected"
|
624
|
+
end
|
625
|
+
|
547
626
|
len += 2
|
548
627
|
|
549
628
|
part = io.read(len)
|
@@ -571,6 +650,10 @@ module Puma
|
|
571
650
|
@partial_part_left = len - part.size
|
572
651
|
end
|
573
652
|
else
|
653
|
+
if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
|
654
|
+
raise HttpParserError, "maximum size of chunk header exceeded"
|
655
|
+
end
|
656
|
+
|
574
657
|
@prev_chunk = line
|
575
658
|
return false
|
576
659
|
end
|
@@ -591,5 +674,9 @@ module Puma
|
|
591
674
|
@requests_served += 1
|
592
675
|
@ready = true
|
593
676
|
end
|
677
|
+
|
678
|
+
def above_http_content_limit(value)
|
679
|
+
@http_content_length_limit&.< value
|
680
|
+
end
|
594
681
|
end
|
595
682
|
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."
|
@@ -115,9 +117,14 @@ module Puma
|
|
115
117
|
|
116
118
|
while restart_server.pop
|
117
119
|
server_thread = server.run
|
120
|
+
|
121
|
+
if @log_writer.debug? && index == 0
|
122
|
+
debug_loaded_extensions "Loaded Extensions - worker 0:"
|
123
|
+
end
|
124
|
+
|
118
125
|
stat_thread ||= Thread.new(@worker_write) do |io|
|
119
126
|
Puma.set_thread_name "stat pld"
|
120
|
-
base_payload = "
|
127
|
+
base_payload = "#{PIPE_PING}#{Process.pid}"
|
121
128
|
|
122
129
|
while true
|
123
130
|
begin
|
@@ -126,7 +133,8 @@ module Puma
|
|
126
133
|
t = server.pool_capacity || 0
|
127
134
|
m = server.max_threads || 0
|
128
135
|
rc = server.requests_count || 0
|
129
|
-
|
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!
|
130
138
|
io << payload
|
131
139
|
rescue IOError
|
132
140
|
Puma::Util.purge_interrupt_queue
|
@@ -142,7 +150,7 @@ module Puma
|
|
142
150
|
# exiting until any background operations are completed
|
143
151
|
@config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
|
144
152
|
ensure
|
145
|
-
@worker_write << "
|
153
|
+
@worker_write << "#{PIPE_TERM}#{Process.pid}\n" rescue nil
|
146
154
|
@worker_write.close
|
147
155
|
end
|
148
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
@@ -6,8 +6,6 @@ require_relative 'plugin'
|
|
6
6
|
require_relative 'cluster/worker_handle'
|
7
7
|
require_relative 'cluster/worker'
|
8
8
|
|
9
|
-
require 'time'
|
10
|
-
|
11
9
|
module Puma
|
12
10
|
# This class is instantiated by the `Puma::Launcher` and used
|
13
11
|
# to boot and serve a Ruby application when puma "workers" are needed
|
@@ -87,10 +85,12 @@ module Puma
|
|
87
85
|
@workers << WorkerHandle.new(idx, pid, @phase, @options)
|
88
86
|
end
|
89
87
|
|
90
|
-
if @options[:fork_worker] &&
|
91
|
-
@workers.all? {|x| x.phase == @phase}
|
92
|
-
|
88
|
+
if @options[:fork_worker] && all_workers_in_phase?
|
93
89
|
@fork_writer << "0\n"
|
90
|
+
|
91
|
+
if worker_at(0).phase > 0
|
92
|
+
@fork_writer << "-2\n"
|
93
|
+
end
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
@@ -150,10 +150,22 @@ module Puma
|
|
150
150
|
idx
|
151
151
|
end
|
152
152
|
|
153
|
+
def worker_at(idx)
|
154
|
+
@workers.find { |w| w.index == idx }
|
155
|
+
end
|
156
|
+
|
153
157
|
def all_workers_booted?
|
154
158
|
@workers.count { |w| !w.booted? } == 0
|
155
159
|
end
|
156
160
|
|
161
|
+
def all_workers_in_phase?
|
162
|
+
@workers.all? { |w| w.phase == @phase }
|
163
|
+
end
|
164
|
+
|
165
|
+
def all_workers_idle_timed_out?
|
166
|
+
(@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
|
167
|
+
end
|
168
|
+
|
157
169
|
def check_workers
|
158
170
|
return if @next_check >= Time.now
|
159
171
|
|
@@ -252,18 +264,18 @@ module Puma
|
|
252
264
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
253
265
|
worker_status = @workers.map do |w|
|
254
266
|
{
|
255
|
-
started_at: w.started_at
|
267
|
+
started_at: utc_iso8601(w.started_at),
|
256
268
|
pid: w.pid,
|
257
269
|
index: w.index,
|
258
270
|
phase: w.phase,
|
259
271
|
booted: w.booted?,
|
260
|
-
last_checkin: w.last_checkin
|
272
|
+
last_checkin: utc_iso8601(w.last_checkin),
|
261
273
|
last_status: w.last_status,
|
262
274
|
}
|
263
275
|
end
|
264
276
|
|
265
277
|
{
|
266
|
-
started_at: @started_at
|
278
|
+
started_at: utc_iso8601(@started_at),
|
267
279
|
workers: @workers.size,
|
268
280
|
phase: @phase,
|
269
281
|
booted_workers: worker_status.count { |w| w[:booted] },
|
@@ -278,7 +290,7 @@ module Puma
|
|
278
290
|
|
279
291
|
# @version 5.0.0
|
280
292
|
def fork_worker!
|
281
|
-
if (worker =
|
293
|
+
if (worker = worker_at 0)
|
282
294
|
worker.phase += 1
|
283
295
|
end
|
284
296
|
phased_restart(true)
|
@@ -413,6 +425,8 @@ module Puma
|
|
413
425
|
|
414
426
|
@master_read, @worker_write = read, @wakeup
|
415
427
|
|
428
|
+
@options[:worker_write] = @worker_write
|
429
|
+
|
416
430
|
@config.run_hooks(:before_fork, nil, @log_writer)
|
417
431
|
|
418
432
|
spawn_workers
|
@@ -428,6 +442,11 @@ module Puma
|
|
428
442
|
|
429
443
|
while @status == :run
|
430
444
|
begin
|
445
|
+
if @options[:idle_timeout] && all_workers_idle_timed_out?
|
446
|
+
log "- All workers reached idle timeout"
|
447
|
+
break
|
448
|
+
end
|
449
|
+
|
431
450
|
if @phased_restart
|
432
451
|
start_phased_restart
|
433
452
|
@phased_restart = false
|
@@ -439,48 +458,66 @@ module Puma
|
|
439
458
|
|
440
459
|
if read.wait_readable([0, @next_check - Time.now].max)
|
441
460
|
req = read.read_nonblock(1)
|
461
|
+
next unless req
|
442
462
|
|
443
|
-
|
444
|
-
|
463
|
+
if req == PIPE_WAKEUP
|
464
|
+
@next_check = Time.now
|
465
|
+
next
|
466
|
+
end
|
445
467
|
|
446
468
|
result = read.gets
|
447
469
|
pid = result.to_i
|
448
470
|
|
449
|
-
if req ==
|
471
|
+
if req == PIPE_BOOT || req == PIPE_FORK
|
450
472
|
pid, idx = result.split(':').map(&:to_i)
|
451
|
-
w =
|
473
|
+
w = worker_at idx
|
452
474
|
w.pid = pid if w.pid.nil?
|
453
475
|
end
|
454
476
|
|
455
477
|
if w = @workers.find { |x| x.pid == pid }
|
456
478
|
case req
|
457
|
-
when
|
479
|
+
when PIPE_BOOT
|
458
480
|
w.boot!
|
459
481
|
log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
|
460
482
|
@next_check = Time.now
|
461
483
|
workers_not_booted -= 1
|
462
|
-
when
|
484
|
+
when PIPE_EXTERNAL_TERM
|
463
485
|
# external term, see worker method, Signal.trap "SIGTERM"
|
464
486
|
w.term!
|
465
|
-
when
|
487
|
+
when PIPE_TERM
|
466
488
|
w.term unless w.term?
|
467
|
-
when
|
468
|
-
|
489
|
+
when PIPE_PING
|
490
|
+
status = result.sub(/^\d+/,'').chomp
|
491
|
+
w.ping!(status)
|
469
492
|
@events.fire(:ping!, w)
|
493
|
+
|
494
|
+
if in_phased_restart && @options[:fork_worker] && workers_not_booted.positive? && w0 = worker_at(0)
|
495
|
+
w0.ping!(status)
|
496
|
+
@events.fire(:ping!, w0)
|
497
|
+
end
|
498
|
+
|
470
499
|
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
471
500
|
@events.fire_on_booted!
|
501
|
+
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
472
502
|
booted = true
|
473
503
|
end
|
504
|
+
when PIPE_IDLE
|
505
|
+
if idle_workers[pid]
|
506
|
+
idle_workers.delete pid
|
507
|
+
else
|
508
|
+
idle_workers[pid] = true
|
509
|
+
end
|
474
510
|
end
|
475
511
|
else
|
476
512
|
log "! Out-of-sync worker list, no #{pid} worker"
|
477
513
|
end
|
478
514
|
end
|
515
|
+
|
479
516
|
if in_phased_restart && workers_not_booted.zero?
|
480
517
|
@events.fire_on_booted!
|
518
|
+
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
481
519
|
in_phased_restart = false
|
482
520
|
end
|
483
|
-
|
484
521
|
rescue Interrupt
|
485
522
|
@status = :stop
|
486
523
|
end
|
@@ -509,10 +546,31 @@ module Puma
|
|
509
546
|
# loops thru @workers, removing workers that exited, and calling
|
510
547
|
# `#term` if needed
|
511
548
|
def wait_workers
|
549
|
+
# Reap all children, known workers or otherwise.
|
550
|
+
# If puma has PID 1, as it's common in containerized environments,
|
551
|
+
# then it's responsible for reaping orphaned processes, so we must reap
|
552
|
+
# all our dead children, regardless of whether they are workers we spawned
|
553
|
+
# or some reattached processes.
|
554
|
+
reaped_children = {}
|
555
|
+
loop do
|
556
|
+
begin
|
557
|
+
pid, status = Process.wait2(-1, Process::WNOHANG)
|
558
|
+
break unless pid
|
559
|
+
reaped_children[pid] = status
|
560
|
+
rescue Errno::ECHILD
|
561
|
+
break
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
512
565
|
@workers.reject! do |w|
|
513
566
|
next false if w.pid.nil?
|
514
567
|
begin
|
515
|
-
|
568
|
+
# We may need to check the PID individually because:
|
569
|
+
# 1. From Ruby versions 2.6 to 3.2, `Process.detach` can prevent or delay
|
570
|
+
# `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
|
571
|
+
# 2. When `fork_worker` is enabled, some worker may not be direct children,
|
572
|
+
# but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
|
573
|
+
if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
|
516
574
|
true
|
517
575
|
else
|
518
576
|
w.term if w.term?
|
@@ -529,6 +587,11 @@ module Puma
|
|
529
587
|
end
|
530
588
|
end
|
531
589
|
end
|
590
|
+
|
591
|
+
# Log unknown children
|
592
|
+
reaped_children.each do |pid, status|
|
593
|
+
log "! reaped unknown child process pid=#{pid} status=#{status}"
|
594
|
+
end
|
532
595
|
end
|
533
596
|
|
534
597
|
# @version 5.0.0
|
@@ -536,14 +599,22 @@ module Puma
|
|
536
599
|
@workers.each do |w|
|
537
600
|
if !w.term? && w.ping_timeout <= Time.now
|
538
601
|
details = if w.booted?
|
539
|
-
"(
|
602
|
+
"(Worker #{w.index} failed to check in within #{@options[:worker_timeout]} seconds)"
|
540
603
|
else
|
541
|
-
"(
|
604
|
+
"(Worker #{w.index} failed to boot within #{@options[:worker_boot_timeout]} seconds)"
|
542
605
|
end
|
543
606
|
log "! Terminating timed out worker #{details}: #{w.pid}"
|
544
607
|
w.kill
|
545
608
|
end
|
546
609
|
end
|
547
610
|
end
|
611
|
+
|
612
|
+
def idle_timed_out_worker_pids
|
613
|
+
idle_workers.keys
|
614
|
+
end
|
615
|
+
|
616
|
+
def idle_workers
|
617
|
+
@idle_workers ||= {}
|
618
|
+
end
|
548
619
|
end
|
549
620
|
end
|