puma 6.4.1 → 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 +407 -8
- data/README.md +109 -49
- data/docs/deployment.md +58 -23
- data/docs/fork_worker.md +11 -1
- data/docs/java_options.md +54 -0
- data/docs/jungle/README.md +1 -1
- data/docs/kubernetes.md +11 -16
- data/docs/plugins.md +6 -2
- data/docs/restart.md +2 -2
- data/docs/signals.md +21 -21
- data/docs/stats.md +11 -5
- data/docs/systemd.md +14 -5
- data/ext/puma_http11/extconf.rb +20 -32
- data/ext/puma_http11/mini_ssl.c +29 -9
- data/ext/puma_http11/org/jruby/puma/Http11.java +40 -9
- data/ext/puma_http11/puma_http11.c +125 -118
- data/lib/puma/app/status.rb +11 -3
- data/lib/puma/binder.rb +21 -11
- data/lib/puma/cli.rb +10 -8
- data/lib/puma/client.rb +183 -83
- data/lib/puma/cluster/worker.rb +24 -21
- data/lib/puma/cluster/worker_handle.rb +38 -8
- data/lib/puma/cluster.rb +73 -47
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +131 -60
- data/lib/puma/const.rb +31 -12
- data/lib/puma/control_cli.rb +10 -6
- data/lib/puma/detect.rb +2 -0
- data/lib/puma/dsl.rb +411 -121
- data/lib/puma/error_logger.rb +7 -5
- data/lib/puma/events.rb +25 -10
- data/lib/puma/io_buffer.rb +8 -4
- data/lib/puma/jruby_restart.rb +0 -16
- data/lib/puma/launcher/bundle_pruner.rb +1 -1
- data/lib/puma/launcher.rb +73 -55
- data/lib/puma/log_writer.rb +9 -9
- data/lib/puma/minissl/context_builder.rb +1 -0
- data/lib/puma/minissl.rb +1 -1
- data/lib/puma/null_io.rb +26 -0
- 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 +71 -39
- data/lib/puma/runner.rb +15 -17
- data/lib/puma/sd_notify.rb +1 -4
- data/lib/puma/server.rb +134 -73
- data/lib/puma/single.rb +7 -4
- data/lib/puma/state_file.rb +3 -2
- data/lib/puma/thread_pool.rb +57 -80
- data/lib/puma/util.rb +0 -7
- data/lib/puma.rb +10 -0
- data/lib/rack/handler/puma.rb +10 -7
- data/tools/Dockerfile +15 -5
- metadata +14 -15
- 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'
|
|
@@ -51,11 +43,24 @@ module Puma
|
|
|
51
43
|
CHUNK_VALID_ENDING = Const::LINE_END
|
|
52
44
|
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
|
53
45
|
|
|
46
|
+
# The maximum number of bytes we'll buffer looking for a valid
|
|
47
|
+
# chunk header.
|
|
48
|
+
MAX_CHUNK_HEADER_SIZE = 4096
|
|
49
|
+
|
|
50
|
+
# The maximum amount of excess data the client sends
|
|
51
|
+
# using chunk size extensions before we abort the connection.
|
|
52
|
+
MAX_CHUNK_EXCESS = 16 * 1024
|
|
53
|
+
|
|
54
54
|
# Content-Length header value validation
|
|
55
55
|
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
|
56
56
|
|
|
57
57
|
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
|
58
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
|
+
|
|
59
64
|
# The object used for a request with no body. All requests with
|
|
60
65
|
# no body share this one object since it has no state.
|
|
61
66
|
EmptyBody = NullIO.new
|
|
@@ -103,7 +108,8 @@ module Puma
|
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
106
|
-
:tempfile, :io_buffer, :http_content_length_limit_exceeded
|
|
111
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded,
|
|
112
|
+
:requests_served
|
|
107
113
|
|
|
108
114
|
attr_writer :peerip, :http_content_length_limit
|
|
109
115
|
|
|
@@ -125,9 +131,9 @@ module Puma
|
|
|
125
131
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
|
126
132
|
end
|
|
127
133
|
|
|
128
|
-
# For the hijack protocol
|
|
129
|
-
#
|
|
130
|
-
def
|
|
134
|
+
# For the full hijack protocol, `env['rack.hijack']` is set to
|
|
135
|
+
# `client.method :full_hijack`
|
|
136
|
+
def full_hijack
|
|
131
137
|
@hijacked = true
|
|
132
138
|
env[HIJACK_IO] ||= @io
|
|
133
139
|
end
|
|
@@ -142,29 +148,31 @@ module Puma
|
|
|
142
148
|
end
|
|
143
149
|
|
|
144
150
|
# Number of seconds until the timeout elapses.
|
|
151
|
+
# @!attribute [r] timeout
|
|
145
152
|
def timeout
|
|
146
153
|
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
147
154
|
end
|
|
148
155
|
|
|
149
|
-
def reset
|
|
156
|
+
def reset
|
|
150
157
|
@parser.reset
|
|
151
158
|
@io_buffer.reset
|
|
152
159
|
@read_header = true
|
|
153
|
-
@read_proxy = !!@expect_proxy_proto
|
|
160
|
+
@read_proxy = !!@expect_proxy_proto && @requests_served.zero?
|
|
154
161
|
@env = @proto_env.dup
|
|
155
|
-
@body = nil
|
|
156
|
-
@tempfile = nil
|
|
157
162
|
@parsed_bytes = 0
|
|
158
163
|
@ready = false
|
|
159
164
|
@body_remain = 0
|
|
160
165
|
@peerip = nil if @remote_addr_header
|
|
161
166
|
@in_last_chunk = false
|
|
162
167
|
@http_content_length_limit_exceeded = false
|
|
168
|
+
end
|
|
163
169
|
|
|
170
|
+
# only used with back-to-back requests contained in the buffer
|
|
171
|
+
def process_back_to_back_requests
|
|
164
172
|
if @buffer
|
|
165
173
|
return false unless try_to_parse_proxy_protocol
|
|
166
174
|
|
|
167
|
-
@parsed_bytes =
|
|
175
|
+
@parsed_bytes = parser_execute
|
|
168
176
|
|
|
169
177
|
if @parser.finished?
|
|
170
178
|
return setup_body
|
|
@@ -172,47 +180,67 @@ module Puma
|
|
|
172
180
|
raise HttpParserError,
|
|
173
181
|
"HEADER is longer than allowed, aborting client early."
|
|
174
182
|
end
|
|
175
|
-
|
|
176
|
-
return false
|
|
177
|
-
else
|
|
178
|
-
begin
|
|
179
|
-
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
|
180
|
-
return try_to_finish
|
|
181
|
-
end
|
|
182
|
-
rescue IOError
|
|
183
|
-
# swallow it
|
|
184
|
-
end
|
|
185
|
-
|
|
186
183
|
end
|
|
187
184
|
end
|
|
188
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
|
+
|
|
189
192
|
def close
|
|
193
|
+
tempfile_close
|
|
190
194
|
begin
|
|
191
195
|
@io.close
|
|
192
196
|
rescue IOError, Errno::EBADF
|
|
193
|
-
Puma::Util.purge_interrupt_queue
|
|
194
197
|
end
|
|
195
198
|
end
|
|
196
199
|
|
|
200
|
+
def tempfile_close
|
|
201
|
+
tf_path = @tempfile&.path
|
|
202
|
+
@tempfile&.close
|
|
203
|
+
File.unlink(tf_path) if tf_path
|
|
204
|
+
@tempfile = nil
|
|
205
|
+
@body = nil
|
|
206
|
+
rescue Errno::ENOENT, IOError
|
|
207
|
+
end
|
|
208
|
+
|
|
197
209
|
# If necessary, read the PROXY protocol from the buffer. Returns
|
|
198
210
|
# false if more data is needed.
|
|
199
211
|
def try_to_parse_proxy_protocol
|
|
200
212
|
if @read_proxy
|
|
201
213
|
if @expect_proxy_proto == :v1
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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"
|
|
206
222
|
end
|
|
207
|
-
|
|
223
|
+
return false
|
|
208
224
|
end
|
|
209
|
-
|
|
210
|
-
# request, this is just HTTP from a non-PROXY client; move on
|
|
225
|
+
|
|
211
226
|
@read_proxy = false
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
return true
|
|
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"
|
|
215
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
|
|
216
244
|
end
|
|
217
245
|
end
|
|
218
246
|
true
|
|
@@ -232,6 +260,7 @@ module Puma
|
|
|
232
260
|
|
|
233
261
|
return read_body if in_data_phase
|
|
234
262
|
|
|
263
|
+
data = nil
|
|
235
264
|
begin
|
|
236
265
|
data = @io.read_nonblock(CHUNK_SIZE)
|
|
237
266
|
rescue IO::WaitReadable
|
|
@@ -257,26 +286,28 @@ module Puma
|
|
|
257
286
|
|
|
258
287
|
return false unless try_to_parse_proxy_protocol
|
|
259
288
|
|
|
260
|
-
@parsed_bytes =
|
|
289
|
+
@parsed_bytes = parser_execute
|
|
261
290
|
|
|
262
291
|
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
|
263
292
|
@http_content_length_limit_exceeded = true
|
|
264
293
|
end
|
|
265
294
|
|
|
266
295
|
if @parser.finished?
|
|
267
|
-
|
|
296
|
+
setup_body
|
|
268
297
|
elsif @parsed_bytes >= MAX_HEADER
|
|
269
298
|
raise HttpParserError,
|
|
270
299
|
"HEADER is longer than allowed, aborting client early."
|
|
300
|
+
else
|
|
301
|
+
false
|
|
271
302
|
end
|
|
272
|
-
|
|
273
|
-
false
|
|
274
303
|
end
|
|
275
304
|
|
|
276
305
|
def eagerly_finish
|
|
277
306
|
return true if @ready
|
|
278
|
-
|
|
279
|
-
|
|
307
|
+
while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
|
|
308
|
+
return true if try_to_finish
|
|
309
|
+
end
|
|
310
|
+
false
|
|
280
311
|
end
|
|
281
312
|
|
|
282
313
|
def finish(timeout)
|
|
@@ -284,6 +315,44 @@ module Puma
|
|
|
284
315
|
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
|
285
316
|
end
|
|
286
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
|
+
|
|
287
356
|
def timeout!
|
|
288
357
|
write_error(408) if in_data_phase
|
|
289
358
|
raise ConnectionError
|
|
@@ -358,17 +427,18 @@ module Puma
|
|
|
358
427
|
if te
|
|
359
428
|
te_lwr = te.downcase
|
|
360
429
|
if te.include? ','
|
|
361
|
-
te_ary = te_lwr.split
|
|
430
|
+
te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
|
|
362
431
|
te_count = te_ary.count CHUNKED
|
|
363
432
|
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
|
364
|
-
if
|
|
365
|
-
@env.delete TRANSFER_ENCODING2
|
|
366
|
-
return setup_chunked_body body
|
|
367
|
-
elsif te_count >= 1
|
|
433
|
+
if te_count > 1
|
|
368
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}'"
|
|
369
437
|
elsif !te_valid
|
|
370
438
|
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
|
371
439
|
end
|
|
440
|
+
@env.delete TRANSFER_ENCODING2
|
|
441
|
+
return setup_chunked_body body
|
|
372
442
|
elsif te_lwr == CHUNKED
|
|
373
443
|
@env.delete TRANSFER_ENCODING2
|
|
374
444
|
return setup_chunked_body body
|
|
@@ -395,18 +465,33 @@ module Puma
|
|
|
395
465
|
return true
|
|
396
466
|
end
|
|
397
467
|
|
|
398
|
-
|
|
468
|
+
content_length = cl.to_i
|
|
469
|
+
|
|
470
|
+
remain = content_length - body.bytesize
|
|
399
471
|
|
|
400
472
|
if remain <= 0
|
|
401
|
-
|
|
402
|
-
|
|
473
|
+
# Part of the body is a pipelined request OR garbage. We'll deal with that later.
|
|
474
|
+
if content_length == 0
|
|
475
|
+
@body = EmptyBody
|
|
476
|
+
if body.empty?
|
|
477
|
+
@buffer = nil
|
|
478
|
+
else
|
|
479
|
+
@buffer = body
|
|
480
|
+
end
|
|
481
|
+
elsif remain == 0
|
|
482
|
+
@body = StringIO.new body
|
|
483
|
+
@buffer = nil
|
|
484
|
+
else
|
|
485
|
+
@body = StringIO.new(body[0,content_length])
|
|
486
|
+
@buffer = body[content_length..-1]
|
|
487
|
+
end
|
|
403
488
|
set_ready
|
|
404
489
|
return true
|
|
405
490
|
end
|
|
406
491
|
|
|
407
492
|
if remain > MAX_BODY
|
|
408
|
-
@body = Tempfile.
|
|
409
|
-
@body.
|
|
493
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
494
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
410
495
|
@body.binmode
|
|
411
496
|
@tempfile = @body
|
|
412
497
|
else
|
|
@@ -431,46 +516,42 @@ module Puma
|
|
|
431
516
|
# after this
|
|
432
517
|
remain = @body_remain
|
|
433
518
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
439
528
|
|
|
440
|
-
|
|
441
|
-
chunk
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
529
|
+
# No chunk means a closed socket
|
|
530
|
+
unless chunk
|
|
531
|
+
@body.close
|
|
532
|
+
@buffer = nil
|
|
533
|
+
set_ready
|
|
534
|
+
raise EOFError
|
|
535
|
+
end
|
|
447
536
|
|
|
448
|
-
|
|
449
|
-
unless chunk
|
|
450
|
-
@body.close
|
|
451
|
-
@buffer = nil
|
|
452
|
-
set_ready
|
|
453
|
-
raise EOFError
|
|
537
|
+
remain -= @body.write(chunk)
|
|
454
538
|
end
|
|
455
539
|
|
|
456
|
-
remain -= @body.write(chunk)
|
|
457
|
-
|
|
458
540
|
if remain <= 0
|
|
459
541
|
@body.rewind
|
|
460
542
|
@buffer = nil
|
|
461
543
|
set_ready
|
|
462
|
-
|
|
544
|
+
true
|
|
545
|
+
else
|
|
546
|
+
@body_remain = remain
|
|
547
|
+
false
|
|
463
548
|
end
|
|
464
|
-
|
|
465
|
-
@body_remain = remain
|
|
466
|
-
|
|
467
|
-
false
|
|
468
549
|
end
|
|
469
550
|
|
|
470
551
|
def read_chunked_body
|
|
471
552
|
while true
|
|
472
553
|
begin
|
|
473
|
-
chunk = @io.read_nonblock(
|
|
554
|
+
chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
|
|
474
555
|
rescue IO::WaitReadable
|
|
475
556
|
return false
|
|
476
557
|
rescue SystemCallError, IOError
|
|
@@ -496,9 +577,10 @@ module Puma
|
|
|
496
577
|
@chunked_body = true
|
|
497
578
|
@partial_part_left = 0
|
|
498
579
|
@prev_chunk = ""
|
|
580
|
+
@excess_cr = 0
|
|
499
581
|
|
|
500
|
-
@body = Tempfile.
|
|
501
|
-
@body.
|
|
582
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
583
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
502
584
|
@body.binmode
|
|
503
585
|
@tempfile = @body
|
|
504
586
|
@chunked_content_length = 0
|
|
@@ -577,6 +659,20 @@ module Puma
|
|
|
577
659
|
end
|
|
578
660
|
end
|
|
579
661
|
|
|
662
|
+
# Track the excess as a function of the size of the
|
|
663
|
+
# header vs the size of the actual data. Excess can
|
|
664
|
+
# go negative (and is expected to) when the body is
|
|
665
|
+
# significant.
|
|
666
|
+
# The additional of chunk_hex.size and 2 compensates
|
|
667
|
+
# for a client sending 1 byte in a chunked body over
|
|
668
|
+
# a long period of time, making sure that that client
|
|
669
|
+
# isn't accidentally eventually punished.
|
|
670
|
+
@excess_cr += (line.size - len - chunk_hex.size - 2)
|
|
671
|
+
|
|
672
|
+
if @excess_cr >= MAX_CHUNK_EXCESS
|
|
673
|
+
raise HttpParserError, "Maximum chunk excess detected"
|
|
674
|
+
end
|
|
675
|
+
|
|
580
676
|
len += 2
|
|
581
677
|
|
|
582
678
|
part = io.read(len)
|
|
@@ -604,6 +700,10 @@ module Puma
|
|
|
604
700
|
@partial_part_left = len - part.size
|
|
605
701
|
end
|
|
606
702
|
else
|
|
703
|
+
if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
|
|
704
|
+
raise HttpParserError, "maximum size of chunk header exceeded"
|
|
705
|
+
end
|
|
706
|
+
|
|
607
707
|
@prev_chunk = line
|
|
608
708
|
return false
|
|
609
709
|
end
|
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,36 +86,37 @@ 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
|
|
92
|
+
elsif idx == -2 # refork cycle is done
|
|
93
|
+
@config.run_hooks(:after_refork, nil, @log_writer, @hook_data)
|
|
91
94
|
elsif idx == 0 # restart server
|
|
92
95
|
restart_server << true << false
|
|
93
96
|
else # fork worker
|
|
94
97
|
worker_pids << pid = spawn_worker(idx)
|
|
95
|
-
@worker_write << "
|
|
98
|
+
@worker_write << "#{PIPE_FORK}#{pid}:#{idx}\n" rescue nil
|
|
96
99
|
end
|
|
97
100
|
end
|
|
98
101
|
end
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
Signal.trap "SIGTERM" do
|
|
102
|
-
@worker_write << "
|
|
105
|
+
@worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
|
|
103
106
|
restart_server.clear
|
|
104
|
-
server.stop
|
|
107
|
+
@server.stop
|
|
105
108
|
restart_server << false
|
|
106
109
|
end
|
|
107
110
|
|
|
108
111
|
begin
|
|
109
|
-
@worker_write << "
|
|
112
|
+
@worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
|
|
110
113
|
rescue SystemCallError, IOError
|
|
111
|
-
Puma::Util.purge_interrupt_queue
|
|
112
114
|
STDERR.puts "Master seems to have exited, exiting."
|
|
113
115
|
return
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
while restart_server.pop
|
|
117
|
-
server_thread = server.run
|
|
119
|
+
server_thread = @server.run
|
|
118
120
|
|
|
119
121
|
if @log_writer.debug? && index == 0
|
|
120
122
|
debug_loaded_extensions "Loaded Extensions - worker 0:"
|
|
@@ -122,19 +124,20 @@ 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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
136
140
|
rescue IOError
|
|
137
|
-
Puma::Util.purge_interrupt_queue
|
|
138
141
|
break
|
|
139
142
|
end
|
|
140
143
|
sleep @options[:worker_check_interval]
|
|
@@ -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
|
|
|
@@ -162,7 +165,7 @@ module Puma
|
|
|
162
165
|
launcher: @launcher,
|
|
163
166
|
pipes: { check_pipe: @check_pipe,
|
|
164
167
|
worker_write: @worker_write },
|
|
165
|
-
|
|
168
|
+
app: @app
|
|
166
169
|
new_worker.run
|
|
167
170
|
end
|
|
168
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
|
|
@@ -52,12 +55,39 @@ module Puma
|
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
def ping!(status)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
60
83
|
end
|
|
84
|
+
@last_checkin = Time.now
|
|
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 }
|
|
61
91
|
end
|
|
62
92
|
|
|
63
93
|
# @see Puma::Cluster#check_workers
|