puma 4.3.8 → 5.6.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.md +1543 -521
- data/LICENSE +23 -20
- data/README.md +120 -36
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +63 -26
- data/docs/compile_options.md +21 -0
- data/docs/deployment.md +60 -69
- data/docs/fork_worker.md +33 -0
- data/docs/jungle/README.md +9 -0
- data/{tools → docs}/jungle/rc.d/README.md +1 -1
- data/{tools → docs}/jungle/rc.d/puma +2 -2
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +1 -1
- data/docs/plugins.md +15 -15
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +46 -23
- data/docs/signals.md +13 -11
- data/docs/stats.md +142 -0
- data/docs/systemd.md +85 -128
- data/ext/puma_http11/PumaHttp11Service.java +2 -4
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +51 -9
- 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 +295 -124
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +5 -3
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +51 -51
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +109 -67
- data/ext/puma_http11/puma_http11.c +32 -51
- data/lib/puma/app/status.rb +50 -36
- data/lib/puma/binder.rb +225 -106
- data/lib/puma/cli.rb +24 -18
- data/lib/puma/client.rb +197 -92
- data/lib/puma/cluster/worker.rb +173 -0
- data/lib/puma/cluster/worker_handle.rb +94 -0
- data/lib/puma/cluster.rb +212 -220
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +58 -49
- data/lib/puma/const.rb +26 -9
- data/lib/puma/control_cli.rb +99 -76
- data/lib/puma/detect.rb +29 -2
- data/lib/puma/dsl.rb +368 -96
- data/lib/puma/error_logger.rb +104 -0
- data/lib/puma/events.rb +55 -34
- data/lib/puma/io_buffer.rb +9 -2
- data/lib/puma/jruby_restart.rb +0 -58
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher.rb +128 -46
- data/lib/puma/minissl/context_builder.rb +14 -9
- data/lib/puma/minissl.rb +137 -50
- data/lib/puma/null_io.rb +18 -1
- data/lib/puma/plugin.rb +3 -12
- data/lib/puma/queue_close.rb +26 -0
- data/lib/puma/rack/builder.rb +1 -5
- data/lib/puma/reactor.rb +85 -369
- data/lib/puma/request.rb +489 -0
- data/lib/puma/runner.rb +46 -61
- data/lib/puma/server.rb +292 -751
- data/lib/puma/single.rb +9 -65
- data/lib/puma/state_file.rb +48 -8
- data/lib/puma/systemd.rb +46 -0
- data/lib/puma/thread_pool.rb +125 -57
- data/lib/puma/util.rb +32 -4
- data/lib/puma.rb +48 -0
- data/lib/rack/handler/puma.rb +2 -3
- data/lib/rack/version_restriction.rb +15 -0
- data/tools/{docker/Dockerfile → Dockerfile} +1 -1
- metadata +29 -24
- data/docs/tcp_mode.md +0 -96
- data/ext/puma_http11/io_buffer.c +0 -155
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
- data/lib/puma/accept_nonblock.rb +0 -29
- data/lib/puma/tcp_logger.rb +0 -41
- data/tools/jungle/README.md +0 -19
- data/tools/jungle/init.d/README.md +0 -61
- data/tools/jungle/init.d/puma +0 -421
- data/tools/jungle/init.d/run-puma +0 -18
- data/tools/jungle/upstart/README.md +0 -61
- data/tools/jungle/upstart/puma-manager.conf +0 -31
- data/tools/jungle/upstart/puma.conf +0 -69
- /data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
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,30 @@ 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 = Const::LINE_END
|
49
|
+
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
50
|
+
|
51
|
+
# The maximum number of bytes we'll buffer looking for a valid
|
52
|
+
# chunk header.
|
53
|
+
MAX_CHUNK_HEADER_SIZE = 4096
|
54
|
+
|
55
|
+
# The maximum amount of excess data the client sends
|
56
|
+
# using chunk size extensions before we abort the connection.
|
57
|
+
MAX_CHUNK_EXCESS = 16 * 1024
|
58
|
+
|
59
|
+
# Content-Length header value validation
|
60
|
+
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
61
|
+
|
62
|
+
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
63
|
+
|
39
64
|
# The object used for a request with no body. All requests with
|
40
65
|
# no body share this one object since it has no state.
|
41
66
|
EmptyBody = NullIO.new
|
@@ -56,6 +81,7 @@ module Puma
|
|
56
81
|
@parser = HttpParser.new
|
57
82
|
@parsed_bytes = 0
|
58
83
|
@read_header = true
|
84
|
+
@read_proxy = false
|
59
85
|
@ready = false
|
60
86
|
|
61
87
|
@body = nil
|
@@ -69,7 +95,9 @@ module Puma
|
|
69
95
|
@hijacked = false
|
70
96
|
|
71
97
|
@peerip = nil
|
98
|
+
@listener = nil
|
72
99
|
@remote_addr_header = nil
|
100
|
+
@expect_proxy_proto = false
|
73
101
|
|
74
102
|
@body_remain = 0
|
75
103
|
|
@@ -81,10 +109,17 @@ module Puma
|
|
81
109
|
|
82
110
|
attr_writer :peerip
|
83
111
|
|
84
|
-
attr_accessor :remote_addr_header
|
112
|
+
attr_accessor :remote_addr_header, :listener
|
85
113
|
|
86
114
|
def_delegators :@io, :closed?
|
87
115
|
|
116
|
+
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
117
|
+
# used for MiniSSL::Socket
|
118
|
+
def io_ok?
|
119
|
+
@to_io.is_a?(::BasicSocket) && !closed?
|
120
|
+
end
|
121
|
+
|
122
|
+
# @!attribute [r] inspect
|
88
123
|
def inspect
|
89
124
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
90
125
|
end
|
@@ -96,27 +131,36 @@ module Puma
|
|
96
131
|
env[HIJACK_IO] ||= @io
|
97
132
|
end
|
98
133
|
|
134
|
+
# @!attribute [r] in_data_phase
|
99
135
|
def in_data_phase
|
100
|
-
|
136
|
+
!(@read_header || @read_proxy)
|
101
137
|
end
|
102
138
|
|
103
139
|
def set_timeout(val)
|
104
|
-
@timeout_at =
|
140
|
+
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
|
141
|
+
end
|
142
|
+
|
143
|
+
# Number of seconds until the timeout elapses.
|
144
|
+
def timeout
|
145
|
+
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
105
146
|
end
|
106
147
|
|
107
148
|
def reset(fast_check=true)
|
108
149
|
@parser.reset
|
109
150
|
@read_header = true
|
151
|
+
@read_proxy = !!@expect_proxy_proto
|
110
152
|
@env = @proto_env.dup
|
111
153
|
@body = nil
|
112
154
|
@tempfile = nil
|
113
155
|
@parsed_bytes = 0
|
114
156
|
@ready = false
|
115
157
|
@body_remain = 0
|
116
|
-
@peerip = nil
|
158
|
+
@peerip = nil if @remote_addr_header
|
117
159
|
@in_last_chunk = false
|
118
160
|
|
119
161
|
if @buffer
|
162
|
+
return false unless try_to_parse_proxy_protocol
|
163
|
+
|
120
164
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
121
165
|
|
122
166
|
if @parser.finished?
|
@@ -129,8 +173,7 @@ module Puma
|
|
129
173
|
return false
|
130
174
|
else
|
131
175
|
begin
|
132
|
-
if fast_check &&
|
133
|
-
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
176
|
+
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
134
177
|
return try_to_finish
|
135
178
|
end
|
136
179
|
rescue IOError
|
@@ -143,19 +186,45 @@ module Puma
|
|
143
186
|
def close
|
144
187
|
begin
|
145
188
|
@io.close
|
146
|
-
rescue IOError
|
147
|
-
|
189
|
+
rescue IOError, Errno::EBADF
|
190
|
+
Puma::Util.purge_interrupt_queue
|
148
191
|
end
|
149
192
|
end
|
150
193
|
|
194
|
+
# If necessary, read the PROXY protocol from the buffer. Returns
|
195
|
+
# false if more data is needed.
|
196
|
+
def try_to_parse_proxy_protocol
|
197
|
+
if @read_proxy
|
198
|
+
if @expect_proxy_proto == :v1
|
199
|
+
if @buffer.include? "\r\n"
|
200
|
+
if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
|
201
|
+
if md[1]
|
202
|
+
@peerip = md[1].split(" ")[0]
|
203
|
+
end
|
204
|
+
@buffer = md.post_match
|
205
|
+
end
|
206
|
+
# if the buffer has a \r\n but doesn't have a PROXY protocol
|
207
|
+
# request, this is just HTTP from a non-PROXY client; move on
|
208
|
+
@read_proxy = false
|
209
|
+
return @buffer.size > 0
|
210
|
+
else
|
211
|
+
return false
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
true
|
216
|
+
end
|
217
|
+
|
151
218
|
def try_to_finish
|
152
|
-
return read_body
|
219
|
+
return read_body if in_data_phase
|
153
220
|
|
154
221
|
begin
|
155
222
|
data = @io.read_nonblock(CHUNK_SIZE)
|
156
223
|
rescue IO::WaitReadable
|
157
224
|
return false
|
158
|
-
rescue
|
225
|
+
rescue EOFError
|
226
|
+
# Swallow error, don't log
|
227
|
+
rescue SystemCallError, IOError
|
159
228
|
raise ConnectionError, "Connection error detected during read"
|
160
229
|
end
|
161
230
|
|
@@ -172,6 +241,8 @@ module Puma
|
|
172
241
|
@buffer = data
|
173
242
|
end
|
174
243
|
|
244
|
+
return false unless try_to_parse_proxy_protocol
|
245
|
+
|
175
246
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
176
247
|
|
177
248
|
if @parser.finished?
|
@@ -184,68 +255,20 @@ module Puma
|
|
184
255
|
false
|
185
256
|
end
|
186
257
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
data = @io.sysread_nonblock(CHUNK_SIZE)
|
193
|
-
rescue OpenSSL::SSL::SSLError => e
|
194
|
-
return false if e.kind_of? IO::WaitReadable
|
195
|
-
raise e
|
196
|
-
end
|
197
|
-
|
198
|
-
# No data means a closed socket
|
199
|
-
unless data
|
200
|
-
@buffer = nil
|
201
|
-
set_ready
|
202
|
-
raise EOFError
|
203
|
-
end
|
204
|
-
|
205
|
-
if @buffer
|
206
|
-
@buffer << data
|
207
|
-
else
|
208
|
-
@buffer = data
|
209
|
-
end
|
210
|
-
|
211
|
-
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
212
|
-
|
213
|
-
if @parser.finished?
|
214
|
-
return setup_body
|
215
|
-
elsif @parsed_bytes >= MAX_HEADER
|
216
|
-
raise HttpParserError,
|
217
|
-
"HEADER is longer than allowed, aborting client early."
|
218
|
-
end
|
219
|
-
|
220
|
-
false
|
221
|
-
end
|
222
|
-
|
223
|
-
def eagerly_finish
|
224
|
-
return true if @ready
|
225
|
-
|
226
|
-
if @io.kind_of? OpenSSL::SSL::SSLSocket
|
227
|
-
return true if jruby_start_try_to_finish
|
228
|
-
end
|
229
|
-
|
230
|
-
return false unless IO.select([@to_io], nil, nil, 0)
|
231
|
-
try_to_finish
|
232
|
-
end
|
233
|
-
|
234
|
-
else
|
258
|
+
def eagerly_finish
|
259
|
+
return true if @ready
|
260
|
+
return false unless @to_io.wait_readable(0)
|
261
|
+
try_to_finish
|
262
|
+
end
|
235
263
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
end
|
241
|
-
end # IS_JRUBY
|
264
|
+
def finish(timeout)
|
265
|
+
return if @ready
|
266
|
+
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
267
|
+
end
|
242
268
|
|
243
|
-
def
|
244
|
-
|
245
|
-
|
246
|
-
IO.select([@to_io], nil, nil)
|
247
|
-
end
|
248
|
-
true
|
269
|
+
def timeout!
|
270
|
+
write_error(408) if in_data_phase
|
271
|
+
raise ConnectionError
|
249
272
|
end
|
250
273
|
|
251
274
|
def write_error(status_code)
|
@@ -259,7 +282,7 @@ module Puma
|
|
259
282
|
return @peerip if @peerip
|
260
283
|
|
261
284
|
if @remote_addr_header
|
262
|
-
hdr = (@env[@remote_addr_header] ||
|
285
|
+
hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
|
263
286
|
@peerip = hdr
|
264
287
|
return hdr
|
265
288
|
end
|
@@ -267,6 +290,26 @@ module Puma
|
|
267
290
|
@peerip ||= @io.peeraddr.last
|
268
291
|
end
|
269
292
|
|
293
|
+
# Returns true if the persistent connection can be closed immediately
|
294
|
+
# without waiting for the configured idle/shutdown timeout.
|
295
|
+
# @version 5.0.0
|
296
|
+
#
|
297
|
+
def can_close?
|
298
|
+
# Allow connection to close if we're not in the middle of parsing a request.
|
299
|
+
@parsed_bytes == 0
|
300
|
+
end
|
301
|
+
|
302
|
+
def expect_proxy_proto=(val)
|
303
|
+
if val
|
304
|
+
if @read_header
|
305
|
+
@read_proxy = true
|
306
|
+
end
|
307
|
+
else
|
308
|
+
@read_proxy = false
|
309
|
+
end
|
310
|
+
@expect_proxy_proto = val
|
311
|
+
end
|
312
|
+
|
270
313
|
private
|
271
314
|
|
272
315
|
def setup_body
|
@@ -284,16 +327,27 @@ module Puma
|
|
284
327
|
body = @parser.body
|
285
328
|
|
286
329
|
te = @env[TRANSFER_ENCODING2]
|
287
|
-
|
288
330
|
if te
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
331
|
+
te_lwr = te.downcase
|
332
|
+
if te.include? ','
|
333
|
+
te_ary = te_lwr.split ','
|
334
|
+
te_count = te_ary.count CHUNKED
|
335
|
+
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
336
|
+
if te_ary.last == CHUNKED && te_count == 1 && te_valid
|
337
|
+
@env.delete TRANSFER_ENCODING2
|
338
|
+
return setup_chunked_body body
|
339
|
+
elsif te_count >= 1
|
340
|
+
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
341
|
+
elsif !te_valid
|
342
|
+
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
294
343
|
end
|
295
|
-
elsif
|
296
|
-
|
344
|
+
elsif te_lwr == CHUNKED
|
345
|
+
@env.delete TRANSFER_ENCODING2
|
346
|
+
return setup_chunked_body body
|
347
|
+
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
348
|
+
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
349
|
+
else
|
350
|
+
raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
297
351
|
end
|
298
352
|
end
|
299
353
|
|
@@ -301,7 +355,12 @@ module Puma
|
|
301
355
|
|
302
356
|
cl = @env[CONTENT_LENGTH]
|
303
357
|
|
304
|
-
|
358
|
+
if cl
|
359
|
+
# cannot contain characters that are not \d, or be empty
|
360
|
+
if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
|
361
|
+
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
362
|
+
end
|
363
|
+
else
|
305
364
|
@buffer = body.empty? ? nil : body
|
306
365
|
@body = EmptyBody
|
307
366
|
set_ready
|
@@ -319,6 +378,7 @@ module Puma
|
|
319
378
|
|
320
379
|
if remain > MAX_BODY
|
321
380
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
381
|
+
@body.unlink
|
322
382
|
@body.binmode
|
323
383
|
@tempfile = @body
|
324
384
|
else
|
@@ -331,7 +391,7 @@ module Puma
|
|
331
391
|
|
332
392
|
@body_remain = remain
|
333
393
|
|
334
|
-
|
394
|
+
false
|
335
395
|
end
|
336
396
|
|
337
397
|
def read_body
|
@@ -398,7 +458,7 @@ module Puma
|
|
398
458
|
end
|
399
459
|
|
400
460
|
if decode_chunk(chunk)
|
401
|
-
@env[CONTENT_LENGTH] = @chunked_content_length
|
461
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
402
462
|
return true
|
403
463
|
end
|
404
464
|
end
|
@@ -408,19 +468,21 @@ module Puma
|
|
408
468
|
@chunked_body = true
|
409
469
|
@partial_part_left = 0
|
410
470
|
@prev_chunk = ""
|
471
|
+
@excess_cr = 0
|
411
472
|
|
412
473
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
474
|
+
@body.unlink
|
413
475
|
@body.binmode
|
414
476
|
@tempfile = @body
|
415
|
-
|
416
477
|
@chunked_content_length = 0
|
417
478
|
|
418
479
|
if decode_chunk(body)
|
419
|
-
@env[CONTENT_LENGTH] = @chunked_content_length
|
480
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
420
481
|
return true
|
421
482
|
end
|
422
483
|
end
|
423
484
|
|
485
|
+
# @version 5.0.0
|
424
486
|
def write_chunk(str)
|
425
487
|
@chunked_content_length += @body.write(str)
|
426
488
|
end
|
@@ -434,7 +496,15 @@ module Puma
|
|
434
496
|
chunk = chunk[@partial_part_left..-1]
|
435
497
|
@partial_part_left = 0
|
436
498
|
else
|
437
|
-
|
499
|
+
if @partial_part_left > 2
|
500
|
+
if @partial_part_left == chunk.size + 1
|
501
|
+
# Don't include the last \r
|
502
|
+
write_chunk(chunk[0..(@partial_part_left-3)])
|
503
|
+
else
|
504
|
+
# don't include the last \r\n
|
505
|
+
write_chunk(chunk)
|
506
|
+
end
|
507
|
+
end
|
438
508
|
@partial_part_left -= chunk.size
|
439
509
|
return false
|
440
510
|
end
|
@@ -449,25 +519,51 @@ module Puma
|
|
449
519
|
|
450
520
|
while !io.eof?
|
451
521
|
line = io.gets
|
452
|
-
if line.end_with?(
|
453
|
-
|
522
|
+
if line.end_with?(CHUNK_VALID_ENDING)
|
523
|
+
# Puma doesn't process chunk extensions, but should parse if they're
|
524
|
+
# present, which is the reason for the semicolon regex
|
525
|
+
chunk_hex = line.strip[/\A[^;]+/]
|
526
|
+
if chunk_hex =~ CHUNK_SIZE_INVALID
|
527
|
+
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
528
|
+
end
|
529
|
+
len = chunk_hex.to_i(16)
|
454
530
|
if len == 0
|
455
531
|
@in_last_chunk = true
|
456
532
|
@body.rewind
|
457
533
|
rest = io.read
|
458
|
-
|
459
|
-
if rest.bytesize < last_crlf_size
|
534
|
+
if rest.bytesize < CHUNK_VALID_ENDING_SIZE
|
460
535
|
@buffer = nil
|
461
|
-
@partial_part_left =
|
536
|
+
@partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
|
462
537
|
return false
|
463
538
|
else
|
464
|
-
|
539
|
+
# if the next character is a CRLF, set buffer to everything after that CRLF
|
540
|
+
start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
|
541
|
+
CHUNK_VALID_ENDING_SIZE
|
542
|
+
else # we have started a trailer section, which we do not support. skip it!
|
543
|
+
rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
|
544
|
+
end
|
545
|
+
|
546
|
+
@buffer = rest[start_of_rest..-1]
|
465
547
|
@buffer = nil if @buffer.empty?
|
466
548
|
set_ready
|
467
549
|
return true
|
468
550
|
end
|
469
551
|
end
|
470
552
|
|
553
|
+
# Track the excess as a function of the size of the
|
554
|
+
# header vs the size of the actual data. Excess can
|
555
|
+
# go negative (and is expected to) when the body is
|
556
|
+
# significant.
|
557
|
+
# The additional of chunk_hex.size and 2 compensates
|
558
|
+
# for a client sending 1 byte in a chunked body over
|
559
|
+
# a long period of time, making sure that that client
|
560
|
+
# isn't accidentally eventually punished.
|
561
|
+
@excess_cr += (line.size - len - chunk_hex.size - 2)
|
562
|
+
|
563
|
+
if @excess_cr >= MAX_CHUNK_EXCESS
|
564
|
+
raise HttpParserError, "Maximum chunk excess detected"
|
565
|
+
end
|
566
|
+
|
471
567
|
len += 2
|
472
568
|
|
473
569
|
part = io.read(len)
|
@@ -481,7 +577,12 @@ module Puma
|
|
481
577
|
|
482
578
|
case
|
483
579
|
when got == len
|
484
|
-
|
580
|
+
# proper chunked segment must end with "\r\n"
|
581
|
+
if part.end_with? CHUNK_VALID_ENDING
|
582
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
583
|
+
else
|
584
|
+
raise HttpParserError, "Chunk size mismatch"
|
585
|
+
end
|
485
586
|
when got <= len - 2
|
486
587
|
write_chunk(part)
|
487
588
|
@partial_part_left = len - part.size
|
@@ -490,6 +591,10 @@ module Puma
|
|
490
591
|
@partial_part_left = len - part.size
|
491
592
|
end
|
492
593
|
else
|
594
|
+
if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
|
595
|
+
raise HttpParserError, "maximum size of chunk header exceeded"
|
596
|
+
end
|
597
|
+
|
493
598
|
@prev_chunk = line
|
494
599
|
return false
|
495
600
|
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puma
|
4
|
+
class Cluster < Puma::Runner
|
5
|
+
# This class is instantiated by the `Puma::Cluster` and represents a single
|
6
|
+
# worker process.
|
7
|
+
#
|
8
|
+
# At the core of this class is running an instance of `Puma::Server` which
|
9
|
+
# gets created via the `start_server` method from the `Puma::Runner` class
|
10
|
+
# that this inherits from.
|
11
|
+
class Worker < Puma::Runner
|
12
|
+
attr_reader :index, :master
|
13
|
+
|
14
|
+
def initialize(index:, master:, launcher:, pipes:, server: nil)
|
15
|
+
super launcher, launcher.events
|
16
|
+
|
17
|
+
@index = index
|
18
|
+
@master = master
|
19
|
+
@launcher = launcher
|
20
|
+
@options = launcher.options
|
21
|
+
@check_pipe = pipes[:check_pipe]
|
22
|
+
@worker_write = pipes[:worker_write]
|
23
|
+
@fork_pipe = pipes[:fork_pipe]
|
24
|
+
@wakeup = pipes[:wakeup]
|
25
|
+
@server = server
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
title = "puma: cluster worker #{index}: #{master}"
|
30
|
+
title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
|
31
|
+
$0 = title
|
32
|
+
|
33
|
+
Signal.trap "SIGINT", "IGNORE"
|
34
|
+
Signal.trap "SIGCHLD", "DEFAULT"
|
35
|
+
|
36
|
+
Thread.new do
|
37
|
+
Puma.set_thread_name "wrkr check"
|
38
|
+
@check_pipe.wait_readable
|
39
|
+
log "! Detected parent died, dying"
|
40
|
+
exit! 1
|
41
|
+
end
|
42
|
+
|
43
|
+
# If we're not running under a Bundler context, then
|
44
|
+
# report the info about the context we will be using
|
45
|
+
if !ENV['BUNDLE_GEMFILE']
|
46
|
+
if File.exist?("Gemfile")
|
47
|
+
log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
|
48
|
+
elsif File.exist?("gems.rb")
|
49
|
+
log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Invoke any worker boot hooks so they can get
|
54
|
+
# things in shape before booting the app.
|
55
|
+
@launcher.config.run_hooks :before_worker_boot, index, @launcher.events
|
56
|
+
|
57
|
+
begin
|
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
|
+
|
65
|
+
restart_server = Queue.new << true << false
|
66
|
+
|
67
|
+
fork_worker = @options[:fork_worker] && index == 0
|
68
|
+
|
69
|
+
if fork_worker
|
70
|
+
restart_server.clear
|
71
|
+
worker_pids = []
|
72
|
+
Signal.trap "SIGCHLD" do
|
73
|
+
wakeup! if worker_pids.reject! do |p|
|
74
|
+
Process.wait(p, Process::WNOHANG) rescue true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Thread.new do
|
79
|
+
Puma.set_thread_name "wrkr fork"
|
80
|
+
while (idx = @fork_pipe.gets)
|
81
|
+
idx = idx.to_i
|
82
|
+
if idx == -1 # stop server
|
83
|
+
if restart_server.length > 0
|
84
|
+
restart_server.clear
|
85
|
+
server.begin_restart(true)
|
86
|
+
@launcher.config.run_hooks :before_refork, nil, @launcher.events
|
87
|
+
Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
|
88
|
+
end
|
89
|
+
elsif idx == 0 # restart server
|
90
|
+
restart_server << true << false
|
91
|
+
else # fork worker
|
92
|
+
worker_pids << pid = spawn_worker(idx)
|
93
|
+
@worker_write << "f#{pid}:#{idx}\n" rescue nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
Signal.trap "SIGTERM" do
|
100
|
+
@worker_write << "e#{Process.pid}\n" rescue nil
|
101
|
+
restart_server.clear
|
102
|
+
server.stop
|
103
|
+
restart_server << false
|
104
|
+
end
|
105
|
+
|
106
|
+
begin
|
107
|
+
@worker_write << "b#{Process.pid}:#{index}\n"
|
108
|
+
rescue SystemCallError, IOError
|
109
|
+
Puma::Util.purge_interrupt_queue
|
110
|
+
STDERR.puts "Master seems to have exited, exiting."
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
while restart_server.pop
|
115
|
+
server_thread = server.run
|
116
|
+
stat_thread ||= Thread.new(@worker_write) do |io|
|
117
|
+
Puma.set_thread_name "stat pld"
|
118
|
+
base_payload = "p#{Process.pid}"
|
119
|
+
|
120
|
+
while true
|
121
|
+
begin
|
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
|
129
|
+
rescue IOError
|
130
|
+
Puma::Util.purge_interrupt_queue
|
131
|
+
break
|
132
|
+
end
|
133
|
+
sleep @options[:worker_check_interval]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
server_thread.join
|
137
|
+
end
|
138
|
+
|
139
|
+
# Invoke any worker shutdown hooks so they can prevent the worker
|
140
|
+
# exiting until any background operations are completed
|
141
|
+
@launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
|
142
|
+
ensure
|
143
|
+
@worker_write << "t#{Process.pid}\n" rescue nil
|
144
|
+
@worker_write.close
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def spawn_worker(idx)
|
150
|
+
@launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
|
151
|
+
|
152
|
+
pid = fork do
|
153
|
+
new_worker = Worker.new index: idx,
|
154
|
+
master: master,
|
155
|
+
launcher: @launcher,
|
156
|
+
pipes: { check_pipe: @check_pipe,
|
157
|
+
worker_write: @worker_write },
|
158
|
+
server: @server
|
159
|
+
new_worker.run
|
160
|
+
end
|
161
|
+
|
162
|
+
if !pid
|
163
|
+
log "! Complete inability to spawn new workers detected"
|
164
|
+
log "! Seppuku is the only choice."
|
165
|
+
exit! 1
|
166
|
+
end
|
167
|
+
|
168
|
+
@launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
|
169
|
+
pid
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|