puma 5.0.4 → 5.6.4
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.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +322 -48
- data/LICENSE +0 -0
- data/README.md +95 -24
- data/bin/puma-wild +0 -0
- data/docs/architecture.md +57 -20
- data/docs/compile_options.md +21 -0
- data/docs/deployment.md +53 -67
- data/docs/fork_worker.md +2 -0
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/jungle/README.md +0 -0
- data/docs/jungle/rc.d/README.md +1 -1
- data/docs/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +0 -0
- data/docs/plugins.md +15 -15
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +7 -7
- data/docs/signals.md +11 -10
- data/docs/stats.md +142 -0
- data/docs/systemd.md +85 -66
- data/ext/puma_http11/PumaHttp11Service.java +0 -0
- data/ext/puma_http11/ext_help.h +0 -0
- data/ext/puma_http11/extconf.rb +42 -6
- data/ext/puma_http11/http11_parser.c +68 -57
- data/ext/puma_http11/http11_parser.h +1 -1
- data/ext/puma_http11/http11_parser.java.rl +1 -1
- data/ext/puma_http11/http11_parser.rl +1 -1
- data/ext/puma_http11/http11_parser_common.rl +1 -1
- data/ext/puma_http11/mini_ssl.c +226 -88
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +0 -0
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +51 -51
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +28 -43
- data/ext/puma_http11/puma_http11.c +9 -3
- data/lib/puma/app/status.rb +4 -7
- data/lib/puma/binder.rb +138 -49
- data/lib/puma/cli.rb +18 -4
- data/lib/puma/client.rb +113 -31
- data/lib/puma/cluster/worker.rb +22 -19
- data/lib/puma/cluster/worker_handle.rb +13 -2
- data/lib/puma/cluster.rb +75 -33
- data/lib/puma/commonlogger.rb +0 -0
- data/lib/puma/configuration.rb +21 -2
- data/lib/puma/const.rb +17 -8
- data/lib/puma/control_cli.rb +76 -71
- data/lib/puma/detect.rb +19 -9
- data/lib/puma/dsl.rb +225 -31
- data/lib/puma/error_logger.rb +12 -5
- data/lib/puma/events.rb +18 -3
- data/lib/puma/io_buffer.rb +0 -0
- data/lib/puma/jruby_restart.rb +0 -0
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher.rb +56 -7
- data/lib/puma/minissl/context_builder.rb +14 -6
- data/lib/puma/minissl.rb +72 -40
- data/lib/puma/null_io.rb +12 -0
- data/lib/puma/plugin/tmp_restart.rb +0 -0
- data/lib/puma/plugin.rb +2 -2
- data/lib/puma/queue_close.rb +7 -7
- data/lib/puma/rack/builder.rb +1 -1
- data/lib/puma/rack/urlmap.rb +0 -0
- data/lib/puma/rack_default.rb +0 -0
- data/lib/puma/reactor.rb +19 -12
- data/lib/puma/request.rb +55 -21
- data/lib/puma/runner.rb +39 -13
- data/lib/puma/server.rb +78 -142
- data/lib/puma/single.rb +0 -0
- data/lib/puma/state_file.rb +45 -9
- data/lib/puma/systemd.rb +46 -0
- data/lib/puma/thread_pool.rb +11 -8
- data/lib/puma/util.rb +8 -1
- data/lib/puma.rb +36 -10
- data/lib/rack/handler/puma.rb +1 -0
- data/tools/Dockerfile +1 -1
- data/tools/trickletest.rb +0 -0
- metadata +15 -9
data/lib/puma/client.rb
CHANGED
@@ -23,6 +23,8 @@ module Puma
|
|
23
23
|
|
24
24
|
class ConnectionError < RuntimeError; end
|
25
25
|
|
26
|
+
class HttpParserError501 < IOError; end
|
27
|
+
|
26
28
|
# An instance of this class represents a unique request from a client.
|
27
29
|
# For example, this could be a web request from a browser or from CURL.
|
28
30
|
#
|
@@ -35,7 +37,21 @@ module Puma
|
|
35
37
|
# Instances of this class are responsible for knowing if
|
36
38
|
# the header and body are fully buffered via the `try_to_finish` method.
|
37
39
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
40
|
+
#
|
38
41
|
class Client
|
42
|
+
|
43
|
+
# this tests all values but the last, which must be chunked
|
44
|
+
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
45
|
+
|
46
|
+
# chunked body validation
|
47
|
+
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
48
|
+
CHUNK_VALID_ENDING = "\r\n".freeze
|
49
|
+
|
50
|
+
# Content-Length header value validation
|
51
|
+
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
52
|
+
|
53
|
+
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
54
|
+
|
39
55
|
# The object used for a request with no body. All requests with
|
40
56
|
# no body share this one object since it has no state.
|
41
57
|
EmptyBody = NullIO.new
|
@@ -56,6 +72,7 @@ module Puma
|
|
56
72
|
@parser = HttpParser.new
|
57
73
|
@parsed_bytes = 0
|
58
74
|
@read_header = true
|
75
|
+
@read_proxy = false
|
59
76
|
@ready = false
|
60
77
|
|
61
78
|
@body = nil
|
@@ -69,7 +86,9 @@ module Puma
|
|
69
86
|
@hijacked = false
|
70
87
|
|
71
88
|
@peerip = nil
|
89
|
+
@listener = nil
|
72
90
|
@remote_addr_header = nil
|
91
|
+
@expect_proxy_proto = false
|
73
92
|
|
74
93
|
@body_remain = 0
|
75
94
|
|
@@ -81,7 +100,7 @@ module Puma
|
|
81
100
|
|
82
101
|
attr_writer :peerip
|
83
102
|
|
84
|
-
attr_accessor :remote_addr_header
|
103
|
+
attr_accessor :remote_addr_header, :listener
|
85
104
|
|
86
105
|
def_delegators :@io, :closed?
|
87
106
|
|
@@ -105,7 +124,7 @@ module Puma
|
|
105
124
|
|
106
125
|
# @!attribute [r] in_data_phase
|
107
126
|
def in_data_phase
|
108
|
-
|
127
|
+
!(@read_header || @read_proxy)
|
109
128
|
end
|
110
129
|
|
111
130
|
def set_timeout(val)
|
@@ -120,16 +139,19 @@ module Puma
|
|
120
139
|
def reset(fast_check=true)
|
121
140
|
@parser.reset
|
122
141
|
@read_header = true
|
142
|
+
@read_proxy = !!@expect_proxy_proto
|
123
143
|
@env = @proto_env.dup
|
124
144
|
@body = nil
|
125
145
|
@tempfile = nil
|
126
146
|
@parsed_bytes = 0
|
127
147
|
@ready = false
|
128
148
|
@body_remain = 0
|
129
|
-
@peerip = nil
|
149
|
+
@peerip = nil if @remote_addr_header
|
130
150
|
@in_last_chunk = false
|
131
151
|
|
132
152
|
if @buffer
|
153
|
+
return false unless try_to_parse_proxy_protocol
|
154
|
+
|
133
155
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
134
156
|
|
135
157
|
if @parser.finished?
|
@@ -142,8 +164,7 @@ module Puma
|
|
142
164
|
return false
|
143
165
|
else
|
144
166
|
begin
|
145
|
-
if fast_check &&
|
146
|
-
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
167
|
+
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
147
168
|
return try_to_finish
|
148
169
|
end
|
149
170
|
rescue IOError
|
@@ -156,13 +177,37 @@ module Puma
|
|
156
177
|
def close
|
157
178
|
begin
|
158
179
|
@io.close
|
159
|
-
rescue IOError
|
160
|
-
|
180
|
+
rescue IOError, Errno::EBADF
|
181
|
+
Puma::Util.purge_interrupt_queue
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# If necessary, read the PROXY protocol from the buffer. Returns
|
186
|
+
# false if more data is needed.
|
187
|
+
def try_to_parse_proxy_protocol
|
188
|
+
if @read_proxy
|
189
|
+
if @expect_proxy_proto == :v1
|
190
|
+
if @buffer.include? "\r\n"
|
191
|
+
if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
|
192
|
+
if md[1]
|
193
|
+
@peerip = md[1].split(" ")[0]
|
194
|
+
end
|
195
|
+
@buffer = md.post_match
|
196
|
+
end
|
197
|
+
# if the buffer has a \r\n but doesn't have a PROXY protocol
|
198
|
+
# request, this is just HTTP from a non-PROXY client; move on
|
199
|
+
@read_proxy = false
|
200
|
+
return @buffer.size > 0
|
201
|
+
else
|
202
|
+
return false
|
203
|
+
end
|
204
|
+
end
|
161
205
|
end
|
206
|
+
true
|
162
207
|
end
|
163
208
|
|
164
209
|
def try_to_finish
|
165
|
-
return read_body
|
210
|
+
return read_body if in_data_phase
|
166
211
|
|
167
212
|
begin
|
168
213
|
data = @io.read_nonblock(CHUNK_SIZE)
|
@@ -187,6 +232,8 @@ module Puma
|
|
187
232
|
@buffer = data
|
188
233
|
end
|
189
234
|
|
235
|
+
return false unless try_to_parse_proxy_protocol
|
236
|
+
|
190
237
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
191
238
|
|
192
239
|
if @parser.finished?
|
@@ -201,13 +248,13 @@ module Puma
|
|
201
248
|
|
202
249
|
def eagerly_finish
|
203
250
|
return true if @ready
|
204
|
-
return false unless
|
251
|
+
return false unless @to_io.wait_readable(0)
|
205
252
|
try_to_finish
|
206
253
|
end
|
207
254
|
|
208
255
|
def finish(timeout)
|
209
256
|
return if @ready
|
210
|
-
|
257
|
+
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
211
258
|
end
|
212
259
|
|
213
260
|
def timeout!
|
@@ -239,13 +286,19 @@ module Puma
|
|
239
286
|
# @version 5.0.0
|
240
287
|
#
|
241
288
|
def can_close?
|
242
|
-
# Allow connection to close if
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
289
|
+
# Allow connection to close if we're not in the middle of parsing a request.
|
290
|
+
@parsed_bytes == 0
|
291
|
+
end
|
292
|
+
|
293
|
+
def expect_proxy_proto=(val)
|
294
|
+
if val
|
295
|
+
if @read_header
|
296
|
+
@read_proxy = true
|
297
|
+
end
|
298
|
+
else
|
299
|
+
@read_proxy = false
|
300
|
+
end
|
301
|
+
@expect_proxy_proto = val
|
249
302
|
end
|
250
303
|
|
251
304
|
private
|
@@ -265,16 +318,27 @@ module Puma
|
|
265
318
|
body = @parser.body
|
266
319
|
|
267
320
|
te = @env[TRANSFER_ENCODING2]
|
268
|
-
|
269
321
|
if te
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
322
|
+
te_lwr = te.downcase
|
323
|
+
if te.include? ','
|
324
|
+
te_ary = te_lwr.split ','
|
325
|
+
te_count = te_ary.count CHUNKED
|
326
|
+
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
327
|
+
if te_ary.last == CHUNKED && te_count == 1 && te_valid
|
328
|
+
@env.delete TRANSFER_ENCODING2
|
329
|
+
return setup_chunked_body body
|
330
|
+
elsif te_count >= 1
|
331
|
+
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
332
|
+
elsif !te_valid
|
333
|
+
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
275
334
|
end
|
276
|
-
elsif
|
277
|
-
|
335
|
+
elsif te_lwr == CHUNKED
|
336
|
+
@env.delete TRANSFER_ENCODING2
|
337
|
+
return setup_chunked_body body
|
338
|
+
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
339
|
+
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
340
|
+
else
|
341
|
+
raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
278
342
|
end
|
279
343
|
end
|
280
344
|
|
@@ -282,7 +346,12 @@ module Puma
|
|
282
346
|
|
283
347
|
cl = @env[CONTENT_LENGTH]
|
284
348
|
|
285
|
-
|
349
|
+
if cl
|
350
|
+
# cannot contain characters that are not \d
|
351
|
+
if cl =~ CONTENT_LENGTH_VALUE_INVALID
|
352
|
+
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
353
|
+
end
|
354
|
+
else
|
286
355
|
@buffer = body.empty? ? nil : body
|
287
356
|
@body = EmptyBody
|
288
357
|
set_ready
|
@@ -300,6 +369,7 @@ module Puma
|
|
300
369
|
|
301
370
|
if remain > MAX_BODY
|
302
371
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
372
|
+
@body.unlink
|
303
373
|
@body.binmode
|
304
374
|
@tempfile = @body
|
305
375
|
else
|
@@ -312,7 +382,7 @@ module Puma
|
|
312
382
|
|
313
383
|
@body_remain = remain
|
314
384
|
|
315
|
-
|
385
|
+
false
|
316
386
|
end
|
317
387
|
|
318
388
|
def read_body
|
@@ -379,7 +449,7 @@ module Puma
|
|
379
449
|
end
|
380
450
|
|
381
451
|
if decode_chunk(chunk)
|
382
|
-
@env[CONTENT_LENGTH] = @chunked_content_length
|
452
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
383
453
|
return true
|
384
454
|
end
|
385
455
|
end
|
@@ -391,12 +461,13 @@ module Puma
|
|
391
461
|
@prev_chunk = ""
|
392
462
|
|
393
463
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
464
|
+
@body.unlink
|
394
465
|
@body.binmode
|
395
466
|
@tempfile = @body
|
396
467
|
@chunked_content_length = 0
|
397
468
|
|
398
469
|
if decode_chunk(body)
|
399
|
-
@env[CONTENT_LENGTH] = @chunked_content_length
|
470
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
400
471
|
return true
|
401
472
|
end
|
402
473
|
end
|
@@ -439,7 +510,13 @@ module Puma
|
|
439
510
|
while !io.eof?
|
440
511
|
line = io.gets
|
441
512
|
if line.end_with?("\r\n")
|
442
|
-
|
513
|
+
# Puma doesn't process chunk extensions, but should parse if they're
|
514
|
+
# present, which is the reason for the semicolon regex
|
515
|
+
chunk_hex = line.strip[/\A[^;]+/]
|
516
|
+
if chunk_hex =~ CHUNK_SIZE_INVALID
|
517
|
+
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
518
|
+
end
|
519
|
+
len = chunk_hex.to_i(16)
|
443
520
|
if len == 0
|
444
521
|
@in_last_chunk = true
|
445
522
|
@body.rewind
|
@@ -470,7 +547,12 @@ module Puma
|
|
470
547
|
|
471
548
|
case
|
472
549
|
when got == len
|
473
|
-
|
550
|
+
# proper chunked segment must end with "\r\n"
|
551
|
+
if part.end_with? CHUNK_VALID_ENDING
|
552
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
553
|
+
else
|
554
|
+
raise HttpParserError, "Chunk size mismatch"
|
555
|
+
end
|
474
556
|
when got <= len - 2
|
475
557
|
write_chunk(part)
|
476
558
|
@partial_part_left = len - part.size
|
data/lib/puma/cluster/worker.rb
CHANGED
@@ -34,8 +34,8 @@ module Puma
|
|
34
34
|
Signal.trap "SIGCHLD", "DEFAULT"
|
35
35
|
|
36
36
|
Thread.new do
|
37
|
-
Puma.set_thread_name "
|
38
|
-
|
37
|
+
Puma.set_thread_name "wrkr check"
|
38
|
+
@check_pipe.wait_readable
|
39
39
|
log "! Detected parent died, dying"
|
40
40
|
exit! 1
|
41
41
|
end
|
@@ -54,7 +54,14 @@ module Puma
|
|
54
54
|
# things in shape before booting the app.
|
55
55
|
@launcher.config.run_hooks :before_worker_boot, index, @launcher.events
|
56
56
|
|
57
|
+
begin
|
57
58
|
server = @server ||= start_server
|
59
|
+
rescue Exception => e
|
60
|
+
log "! Unable to start worker"
|
61
|
+
log e.backtrace[0]
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
58
65
|
restart_server = Queue.new << true << false
|
59
66
|
|
60
67
|
fork_worker = @options[:fork_worker] && index == 0
|
@@ -69,7 +76,7 @@ module Puma
|
|
69
76
|
end
|
70
77
|
|
71
78
|
Thread.new do
|
72
|
-
Puma.set_thread_name "
|
79
|
+
Puma.set_thread_name "wrkr fork"
|
73
80
|
while (idx = @fork_pipe.gets)
|
74
81
|
idx = idx.to_i
|
75
82
|
if idx == -1 # stop server
|
@@ -99,7 +106,7 @@ module Puma
|
|
99
106
|
begin
|
100
107
|
@worker_write << "b#{Process.pid}:#{index}\n"
|
101
108
|
rescue SystemCallError, IOError
|
102
|
-
|
109
|
+
Puma::Util.purge_interrupt_queue
|
103
110
|
STDERR.puts "Master seems to have exited, exiting."
|
104
111
|
return
|
105
112
|
end
|
@@ -107,17 +114,23 @@ module Puma
|
|
107
114
|
while restart_server.pop
|
108
115
|
server_thread = server.run
|
109
116
|
stat_thread ||= Thread.new(@worker_write) do |io|
|
110
|
-
Puma.set_thread_name "stat
|
117
|
+
Puma.set_thread_name "stat pld"
|
118
|
+
base_payload = "p#{Process.pid}"
|
111
119
|
|
112
120
|
while true
|
113
121
|
begin
|
114
|
-
|
115
|
-
|
122
|
+
b = server.backlog || 0
|
123
|
+
r = server.running || 0
|
124
|
+
t = server.pool_capacity || 0
|
125
|
+
m = server.max_threads || 0
|
126
|
+
rc = server.requests_count || 0
|
127
|
+
payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
|
128
|
+
io << payload
|
116
129
|
rescue IOError
|
117
|
-
|
130
|
+
Puma::Util.purge_interrupt_queue
|
118
131
|
break
|
119
132
|
end
|
120
|
-
sleep
|
133
|
+
sleep @options[:worker_check_interval]
|
121
134
|
end
|
122
135
|
end
|
123
136
|
server_thread.join
|
@@ -155,16 +168,6 @@ module Puma
|
|
155
168
|
@launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
|
156
169
|
pid
|
157
170
|
end
|
158
|
-
|
159
|
-
def wakeup!
|
160
|
-
return unless @wakeup
|
161
|
-
|
162
|
-
begin
|
163
|
-
@wakeup.write "!" unless @wakeup.closed?
|
164
|
-
rescue SystemCallError, IOError
|
165
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
166
|
-
end
|
167
|
-
end
|
168
171
|
end
|
169
172
|
end
|
170
173
|
end
|
@@ -31,19 +31,30 @@ module Puma
|
|
31
31
|
@stage == :booted
|
32
32
|
end
|
33
33
|
|
34
|
+
def uptime
|
35
|
+
Time.now - started_at
|
36
|
+
end
|
37
|
+
|
34
38
|
def boot!
|
35
39
|
@last_checkin = Time.now
|
36
40
|
@stage = :booted
|
37
41
|
end
|
38
42
|
|
43
|
+
def term!
|
44
|
+
@term = true
|
45
|
+
end
|
46
|
+
|
39
47
|
def term?
|
40
48
|
@term
|
41
49
|
end
|
42
50
|
|
43
51
|
def ping!(status)
|
44
52
|
@last_checkin = Time.now
|
45
|
-
|
46
|
-
@last_status =
|
53
|
+
captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
|
54
|
+
@last_status = captures.names.inject({}) do |hash, key|
|
55
|
+
hash[key.to_sym] = captures[key].to_i
|
56
|
+
hash
|
57
|
+
end
|
47
58
|
end
|
48
59
|
|
49
60
|
# @see Puma::Cluster#check_workers
|
data/lib/puma/cluster.rb
CHANGED
@@ -43,6 +43,7 @@ module Puma
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def start_phased_restart
|
46
|
+
@events.fire_on_restart!
|
46
47
|
@phase += 1
|
47
48
|
log "- Starting phased worker restart, phase: #{@phase}"
|
48
49
|
|
@@ -107,24 +108,42 @@ module Puma
|
|
107
108
|
def cull_workers
|
108
109
|
diff = @workers.size - @options[:workers]
|
109
110
|
return if diff < 1
|
111
|
+
debug "Culling #{diff} workers"
|
110
112
|
|
111
|
-
|
113
|
+
workers = workers_to_cull(diff)
|
114
|
+
debug "Workers to cull: #{workers.inspect}"
|
112
115
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
workers_to_cull.each do |worker|
|
117
|
-
log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
|
116
|
+
workers.each do |worker|
|
117
|
+
log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
|
118
118
|
worker.term
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
+
def workers_to_cull(diff)
|
123
|
+
workers = @workers.sort_by(&:started_at)
|
124
|
+
|
125
|
+
# In fork_worker mode, worker 0 acts as our master process.
|
126
|
+
# We should avoid culling it to preserve copy-on-write memory gains.
|
127
|
+
workers.reject! { |w| w.index == 0 } if @options[:fork_worker]
|
128
|
+
|
129
|
+
workers[cull_start_index(diff), diff]
|
130
|
+
end
|
131
|
+
|
132
|
+
def cull_start_index(diff)
|
133
|
+
case @options[:worker_culling_strategy]
|
134
|
+
when :oldest
|
135
|
+
0
|
136
|
+
else # :youngest
|
137
|
+
-diff
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
122
141
|
# @!attribute [r] next_worker_index
|
123
142
|
def next_worker_index
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
143
|
+
occupied_positions = @workers.map(&:index)
|
144
|
+
idx = 0
|
145
|
+
idx += 1 until !occupied_positions.include?(idx)
|
146
|
+
idx
|
128
147
|
end
|
129
148
|
|
130
149
|
def all_workers_booted?
|
@@ -134,7 +153,7 @@ module Puma
|
|
134
153
|
def check_workers
|
135
154
|
return if @next_check >= Time.now
|
136
155
|
|
137
|
-
@next_check = Time.now +
|
156
|
+
@next_check = Time.now + @options[:worker_check_interval]
|
138
157
|
|
139
158
|
timeout_workers
|
140
159
|
wait_workers
|
@@ -163,16 +182,6 @@ module Puma
|
|
163
182
|
].compact.min
|
164
183
|
end
|
165
184
|
|
166
|
-
def wakeup!
|
167
|
-
return unless @wakeup
|
168
|
-
|
169
|
-
begin
|
170
|
-
@wakeup.write "!" unless @wakeup.closed?
|
171
|
-
rescue SystemCallError, IOError
|
172
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
185
|
def worker(index, master)
|
177
186
|
@workers = []
|
178
187
|
|
@@ -317,7 +326,7 @@ module Puma
|
|
317
326
|
|
318
327
|
stop_workers
|
319
328
|
stop
|
320
|
-
|
329
|
+
@events.fire_on_stopped!
|
321
330
|
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
322
331
|
exit 0 # Clean exit, workers were stopped
|
323
332
|
end
|
@@ -329,15 +338,25 @@ module Puma
|
|
329
338
|
|
330
339
|
output_header "cluster"
|
331
340
|
|
332
|
-
|
333
|
-
|
334
|
-
before = Thread.list
|
341
|
+
# This is aligned with the output from Runner, see Runner#output_header
|
342
|
+
log "* Workers: #{@options[:workers]}"
|
335
343
|
|
336
344
|
if preload?
|
345
|
+
# Threads explicitly marked as fork safe will be ignored. Used in Rails,
|
346
|
+
# but may be used by anyone. Note that we need to explicit
|
347
|
+
# Process::Waiter check here because there's a bug in Ruby 2.6 and below
|
348
|
+
# where calling thread_variable_get on a Process::Waiter will segfault.
|
349
|
+
# We can drop that clause once those versions of Ruby are no longer
|
350
|
+
# supported.
|
351
|
+
fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
|
352
|
+
|
353
|
+
before = Thread.list.reject(&fork_safe)
|
354
|
+
|
355
|
+
log "* Restarts: (\u2714) hot (\u2716) phased"
|
337
356
|
log "* Preloading application"
|
338
357
|
load_and_bind
|
339
358
|
|
340
|
-
after = Thread.list
|
359
|
+
after = Thread.list.reject(&fork_safe)
|
341
360
|
|
342
361
|
if after.size > before.size
|
343
362
|
threads = (after - before)
|
@@ -351,7 +370,7 @@ module Puma
|
|
351
370
|
end
|
352
371
|
end
|
353
372
|
else
|
354
|
-
log "*
|
373
|
+
log "* Restarts: (\u2714) hot (\u2714) phased"
|
355
374
|
|
356
375
|
unless @launcher.config.app_configured?
|
357
376
|
error "No application configured, nothing to run"
|
@@ -378,6 +397,8 @@ module Puma
|
|
378
397
|
|
379
398
|
log "Use Ctrl-C to stop"
|
380
399
|
|
400
|
+
single_worker_warning
|
401
|
+
|
381
402
|
redirect_io
|
382
403
|
|
383
404
|
Plugins.fire_background
|
@@ -399,19 +420,21 @@ module Puma
|
|
399
420
|
|
400
421
|
begin
|
401
422
|
booted = false
|
423
|
+
in_phased_restart = false
|
424
|
+
workers_not_booted = @options[:workers]
|
402
425
|
|
403
426
|
while @status == :run
|
404
427
|
begin
|
405
428
|
if @phased_restart
|
406
429
|
start_phased_restart
|
407
430
|
@phased_restart = false
|
431
|
+
in_phased_restart = true
|
432
|
+
workers_not_booted = @options[:workers]
|
408
433
|
end
|
409
434
|
|
410
435
|
check_workers
|
411
436
|
|
412
|
-
|
413
|
-
|
414
|
-
if res
|
437
|
+
if read.wait_readable([0, @next_check - Time.now].max)
|
415
438
|
req = read.read_nonblock(1)
|
416
439
|
|
417
440
|
@next_check = Time.now if req == "!"
|
@@ -430,11 +453,12 @@ module Puma
|
|
430
453
|
case req
|
431
454
|
when "b"
|
432
455
|
w.boot!
|
433
|
-
log "- Worker #{w.index} (
|
456
|
+
log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
|
434
457
|
@next_check = Time.now
|
458
|
+
workers_not_booted -= 1
|
435
459
|
when "e"
|
436
460
|
# external term, see worker method, Signal.trap "SIGTERM"
|
437
|
-
w.
|
461
|
+
w.term!
|
438
462
|
when "t"
|
439
463
|
w.term unless w.term?
|
440
464
|
when "p"
|
@@ -449,6 +473,10 @@ module Puma
|
|
449
473
|
log "! Out-of-sync worker list, no #{pid} worker"
|
450
474
|
end
|
451
475
|
end
|
476
|
+
if in_phased_restart && workers_not_booted.zero?
|
477
|
+
@events.fire_on_booted!
|
478
|
+
in_phased_restart = false
|
479
|
+
end
|
452
480
|
|
453
481
|
rescue Interrupt
|
454
482
|
@status = :stop
|
@@ -466,6 +494,15 @@ module Puma
|
|
466
494
|
|
467
495
|
private
|
468
496
|
|
497
|
+
def single_worker_warning
|
498
|
+
return if @options[:workers] != 1 || @options[:silence_single_worker_warning]
|
499
|
+
|
500
|
+
log "! WARNING: Detected running cluster mode with 1 worker."
|
501
|
+
log "! Running Puma in cluster mode with a single worker is often a misconfiguration."
|
502
|
+
log "! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead."
|
503
|
+
log "! Set the `silence_single_worker_warning` option to silence this warning message."
|
504
|
+
end
|
505
|
+
|
469
506
|
# loops thru @workers, removing workers that exited, and calling
|
470
507
|
# `#term` if needed
|
471
508
|
def wait_workers
|
@@ -495,7 +532,12 @@ module Puma
|
|
495
532
|
def timeout_workers
|
496
533
|
@workers.each do |w|
|
497
534
|
if !w.term? && w.ping_timeout <= Time.now
|
498
|
-
|
535
|
+
details = if w.booted?
|
536
|
+
"(worker failed to check in within #{@options[:worker_timeout]} seconds)"
|
537
|
+
else
|
538
|
+
"(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
|
539
|
+
end
|
540
|
+
log "! Terminating timed out worker #{details}: #{w.pid}"
|
499
541
|
w.kill
|
500
542
|
end
|
501
543
|
end
|
data/lib/puma/commonlogger.rb
CHANGED
File without changes
|