puma 4.3.5 → 6.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +1639 -519
- data/LICENSE +23 -20
- data/README.md +130 -42
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +63 -26
- data/docs/compile_options.md +55 -0
- data/docs/deployment.md +60 -69
- data/docs/fork_worker.md +31 -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 +9 -0
- data/{tools → docs}/jungle/rc.d/README.md +1 -1
- data/{tools → docs}/jungle/rc.d/puma +2 -2
- data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +2 -2
- 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/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -4
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +56 -11
- data/ext/puma_http11/http11_parser.c +69 -58
- data/ext/puma_http11/http11_parser.h +2 -2
- data/ext/puma_http11/http11_parser.java.rl +3 -3
- data/ext/puma_http11/http11_parser.rl +3 -3
- data/ext/puma_http11/http11_parser_common.rl +3 -3
- data/ext/puma_http11/mini_ssl.c +322 -130
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +52 -52
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
- data/ext/puma_http11/puma_http11.c +47 -57
- data/lib/puma/app/status.rb +53 -37
- data/lib/puma/binder.rb +232 -119
- data/lib/puma/cli.rb +33 -33
- data/lib/puma/client.rb +197 -101
- data/lib/puma/cluster/worker.rb +175 -0
- data/lib/puma/cluster/worker_handle.rb +97 -0
- data/lib/puma/cluster.rb +224 -229
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +112 -87
- data/lib/puma/const.rb +30 -25
- data/lib/puma/control_cli.rb +99 -79
- data/lib/puma/detect.rb +31 -2
- data/lib/puma/dsl.rb +426 -110
- data/lib/puma/error_logger.rb +112 -0
- data/lib/puma/events.rb +16 -115
- data/lib/puma/io_buffer.rb +44 -2
- data/lib/puma/jruby_restart.rb +2 -59
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +170 -148
- data/lib/puma/log_writer.rb +137 -0
- data/lib/puma/minissl/context_builder.rb +35 -19
- data/lib/puma/minissl.rb +213 -55
- data/lib/puma/null_io.rb +18 -1
- data/lib/puma/plugin/tmp_restart.rb +1 -1
- data/lib/puma/plugin.rb +3 -12
- data/lib/puma/rack/builder.rb +5 -9
- data/lib/puma/rack/urlmap.rb +0 -0
- data/lib/puma/rack_default.rb +1 -1
- data/lib/puma/reactor.rb +85 -369
- data/lib/puma/request.rb +644 -0
- data/lib/puma/runner.rb +83 -77
- data/lib/puma/server.rb +303 -773
- data/lib/puma/single.rb +18 -74
- data/lib/puma/state_file.rb +45 -8
- data/lib/puma/systemd.rb +47 -0
- data/lib/puma/thread_pool.rb +136 -68
- data/lib/puma/util.rb +21 -4
- data/lib/puma.rb +54 -5
- data/lib/rack/handler/puma.rb +11 -12
- data/tools/{docker/Dockerfile → Dockerfile} +1 -1
- data/tools/trickletest.rb +0 -0
- metadata +36 -28
- 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/lib/puma/client.rb
CHANGED
@@ -8,7 +8,8 @@ class IO
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
require_relative 'detect'
|
12
|
+
require_relative 'io_buffer'
|
12
13
|
require 'tempfile'
|
13
14
|
require 'forwardable'
|
14
15
|
|
@@ -23,6 +24,11 @@ module Puma
|
|
23
24
|
|
24
25
|
class ConnectionError < RuntimeError; end
|
25
26
|
|
27
|
+
class HttpParserError501 < IOError; end
|
28
|
+
|
29
|
+
#———————————————————————— DO NOT USE — this class is for internal use only ———
|
30
|
+
|
31
|
+
|
26
32
|
# An instance of this class represents a unique request from a client.
|
27
33
|
# For example, this could be a web request from a browser or from CURL.
|
28
34
|
#
|
@@ -35,7 +41,21 @@ module Puma
|
|
35
41
|
# Instances of this class are responsible for knowing if
|
36
42
|
# the header and body are fully buffered via the `try_to_finish` method.
|
37
43
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
38
|
-
|
44
|
+
#
|
45
|
+
class Client # :nodoc:
|
46
|
+
|
47
|
+
# this tests all values but the last, which must be chunked
|
48
|
+
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
49
|
+
|
50
|
+
# chunked body validation
|
51
|
+
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
52
|
+
CHUNK_VALID_ENDING = "\r\n".freeze
|
53
|
+
|
54
|
+
# Content-Length header value validation
|
55
|
+
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
56
|
+
|
57
|
+
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
58
|
+
|
39
59
|
# The object used for a request with no body. All requests with
|
40
60
|
# no body share this one object since it has no state.
|
41
61
|
EmptyBody = NullIO.new
|
@@ -46,16 +66,14 @@ module Puma
|
|
46
66
|
def initialize(io, env=nil)
|
47
67
|
@io = io
|
48
68
|
@to_io = io.to_io
|
69
|
+
@io_buffer = IOBuffer.new
|
49
70
|
@proto_env = env
|
50
|
-
|
51
|
-
@env = nil
|
52
|
-
else
|
53
|
-
@env = env.dup
|
54
|
-
end
|
71
|
+
@env = env ? env.dup : nil
|
55
72
|
|
56
73
|
@parser = HttpParser.new
|
57
74
|
@parsed_bytes = 0
|
58
75
|
@read_header = true
|
76
|
+
@read_proxy = false
|
59
77
|
@ready = false
|
60
78
|
|
61
79
|
@body = nil
|
@@ -69,7 +87,10 @@ module Puma
|
|
69
87
|
@hijacked = false
|
70
88
|
|
71
89
|
@peerip = nil
|
90
|
+
@peer_family = nil
|
91
|
+
@listener = nil
|
72
92
|
@remote_addr_header = nil
|
93
|
+
@expect_proxy_proto = false
|
73
94
|
|
74
95
|
@body_remain = 0
|
75
96
|
|
@@ -77,14 +98,21 @@ module Puma
|
|
77
98
|
end
|
78
99
|
|
79
100
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
80
|
-
:tempfile
|
101
|
+
:tempfile, :io_buffer
|
81
102
|
|
82
103
|
attr_writer :peerip
|
83
104
|
|
84
|
-
attr_accessor :remote_addr_header
|
105
|
+
attr_accessor :remote_addr_header, :listener
|
85
106
|
|
86
107
|
def_delegators :@io, :closed?
|
87
108
|
|
109
|
+
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
110
|
+
# used for MiniSSL::Socket
|
111
|
+
def io_ok?
|
112
|
+
@to_io.is_a?(::BasicSocket) && !closed?
|
113
|
+
end
|
114
|
+
|
115
|
+
# @!attribute [r] inspect
|
88
116
|
def inspect
|
89
117
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
90
118
|
end
|
@@ -96,27 +124,37 @@ module Puma
|
|
96
124
|
env[HIJACK_IO] ||= @io
|
97
125
|
end
|
98
126
|
|
127
|
+
# @!attribute [r] in_data_phase
|
99
128
|
def in_data_phase
|
100
|
-
|
129
|
+
!(@read_header || @read_proxy)
|
101
130
|
end
|
102
131
|
|
103
132
|
def set_timeout(val)
|
104
|
-
@timeout_at =
|
133
|
+
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
|
134
|
+
end
|
135
|
+
|
136
|
+
# Number of seconds until the timeout elapses.
|
137
|
+
def timeout
|
138
|
+
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
105
139
|
end
|
106
140
|
|
107
141
|
def reset(fast_check=true)
|
108
142
|
@parser.reset
|
143
|
+
@io_buffer.reset
|
109
144
|
@read_header = true
|
145
|
+
@read_proxy = !!@expect_proxy_proto
|
110
146
|
@env = @proto_env.dup
|
111
147
|
@body = nil
|
112
148
|
@tempfile = nil
|
113
149
|
@parsed_bytes = 0
|
114
150
|
@ready = false
|
115
151
|
@body_remain = 0
|
116
|
-
@peerip = nil
|
152
|
+
@peerip = nil if @remote_addr_header
|
117
153
|
@in_last_chunk = false
|
118
154
|
|
119
155
|
if @buffer
|
156
|
+
return false unless try_to_parse_proxy_protocol
|
157
|
+
|
120
158
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
121
159
|
|
122
160
|
if @parser.finished?
|
@@ -129,8 +167,7 @@ module Puma
|
|
129
167
|
return false
|
130
168
|
else
|
131
169
|
begin
|
132
|
-
if fast_check &&
|
133
|
-
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
170
|
+
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
134
171
|
return try_to_finish
|
135
172
|
end
|
136
173
|
rescue IOError
|
@@ -143,19 +180,45 @@ module Puma
|
|
143
180
|
def close
|
144
181
|
begin
|
145
182
|
@io.close
|
146
|
-
rescue IOError
|
147
|
-
|
183
|
+
rescue IOError, Errno::EBADF
|
184
|
+
Puma::Util.purge_interrupt_queue
|
148
185
|
end
|
149
186
|
end
|
150
187
|
|
188
|
+
# If necessary, read the PROXY protocol from the buffer. Returns
|
189
|
+
# false if more data is needed.
|
190
|
+
def try_to_parse_proxy_protocol
|
191
|
+
if @read_proxy
|
192
|
+
if @expect_proxy_proto == :v1
|
193
|
+
if @buffer.include? "\r\n"
|
194
|
+
if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
|
195
|
+
if md[1]
|
196
|
+
@peerip = md[1].split(" ")[0]
|
197
|
+
end
|
198
|
+
@buffer = md.post_match
|
199
|
+
end
|
200
|
+
# if the buffer has a \r\n but doesn't have a PROXY protocol
|
201
|
+
# request, this is just HTTP from a non-PROXY client; move on
|
202
|
+
@read_proxy = false
|
203
|
+
return @buffer.size > 0
|
204
|
+
else
|
205
|
+
return false
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
true
|
210
|
+
end
|
211
|
+
|
151
212
|
def try_to_finish
|
152
|
-
return read_body
|
213
|
+
return read_body if in_data_phase
|
153
214
|
|
154
215
|
begin
|
155
216
|
data = @io.read_nonblock(CHUNK_SIZE)
|
156
|
-
rescue
|
217
|
+
rescue IO::WaitReadable
|
157
218
|
return false
|
158
|
-
rescue
|
219
|
+
rescue EOFError
|
220
|
+
# Swallow error, don't log
|
221
|
+
rescue SystemCallError, IOError
|
159
222
|
raise ConnectionError, "Connection error detected during read"
|
160
223
|
end
|
161
224
|
|
@@ -172,6 +235,8 @@ module Puma
|
|
172
235
|
@buffer = data
|
173
236
|
end
|
174
237
|
|
238
|
+
return false unless try_to_parse_proxy_protocol
|
239
|
+
|
175
240
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
176
241
|
|
177
242
|
if @parser.finished?
|
@@ -184,68 +249,20 @@ module Puma
|
|
184
249
|
false
|
185
250
|
end
|
186
251
|
|
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
|
252
|
+
def eagerly_finish
|
253
|
+
return true if @ready
|
254
|
+
return false unless @to_io.wait_readable(0)
|
255
|
+
try_to_finish
|
256
|
+
end
|
235
257
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
end
|
241
|
-
end # IS_JRUBY
|
258
|
+
def finish(timeout)
|
259
|
+
return if @ready
|
260
|
+
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
261
|
+
end
|
242
262
|
|
243
|
-
def
|
244
|
-
|
245
|
-
|
246
|
-
IO.select([@to_io], nil, nil)
|
247
|
-
end
|
248
|
-
true
|
263
|
+
def timeout!
|
264
|
+
write_error(408) if in_data_phase
|
265
|
+
raise ConnectionError
|
249
266
|
end
|
250
267
|
|
251
268
|
def write_error(status_code)
|
@@ -259,7 +276,7 @@ module Puma
|
|
259
276
|
return @peerip if @peerip
|
260
277
|
|
261
278
|
if @remote_addr_header
|
262
|
-
hdr = (@env[@remote_addr_header] ||
|
279
|
+
hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
|
263
280
|
@peerip = hdr
|
264
281
|
return hdr
|
265
282
|
end
|
@@ -267,10 +284,40 @@ module Puma
|
|
267
284
|
@peerip ||= @io.peeraddr.last
|
268
285
|
end
|
269
286
|
|
287
|
+
def peer_family
|
288
|
+
return @peer_family if @peer_family
|
289
|
+
|
290
|
+
@peer_family ||= begin
|
291
|
+
@io.local_address.afamily
|
292
|
+
rescue
|
293
|
+
Socket::AF_INET
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Returns true if the persistent connection can be closed immediately
|
298
|
+
# without waiting for the configured idle/shutdown timeout.
|
299
|
+
# @version 5.0.0
|
300
|
+
#
|
301
|
+
def can_close?
|
302
|
+
# Allow connection to close if we're not in the middle of parsing a request.
|
303
|
+
@parsed_bytes == 0
|
304
|
+
end
|
305
|
+
|
306
|
+
def expect_proxy_proto=(val)
|
307
|
+
if val
|
308
|
+
if @read_header
|
309
|
+
@read_proxy = true
|
310
|
+
end
|
311
|
+
else
|
312
|
+
@read_proxy = false
|
313
|
+
end
|
314
|
+
@expect_proxy_proto = val
|
315
|
+
end
|
316
|
+
|
270
317
|
private
|
271
318
|
|
272
319
|
def setup_body
|
273
|
-
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
320
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
274
321
|
|
275
322
|
if @env[HTTP_EXPECT] == CONTINUE
|
276
323
|
# TODO allow a hook here to check the headers before
|
@@ -284,16 +331,27 @@ module Puma
|
|
284
331
|
body = @parser.body
|
285
332
|
|
286
333
|
te = @env[TRANSFER_ENCODING2]
|
287
|
-
|
288
334
|
if te
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
335
|
+
te_lwr = te.downcase
|
336
|
+
if te.include? ','
|
337
|
+
te_ary = te_lwr.split ','
|
338
|
+
te_count = te_ary.count CHUNKED
|
339
|
+
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
340
|
+
if te_ary.last == CHUNKED && te_count == 1 && te_valid
|
341
|
+
@env.delete TRANSFER_ENCODING2
|
342
|
+
return setup_chunked_body body
|
343
|
+
elsif te_count >= 1
|
344
|
+
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
345
|
+
elsif !te_valid
|
346
|
+
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
294
347
|
end
|
295
|
-
elsif
|
296
|
-
|
348
|
+
elsif te_lwr == CHUNKED
|
349
|
+
@env.delete TRANSFER_ENCODING2
|
350
|
+
return setup_chunked_body body
|
351
|
+
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
352
|
+
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
353
|
+
else
|
354
|
+
raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
297
355
|
end
|
298
356
|
end
|
299
357
|
|
@@ -301,7 +359,12 @@ module Puma
|
|
301
359
|
|
302
360
|
cl = @env[CONTENT_LENGTH]
|
303
361
|
|
304
|
-
|
362
|
+
if cl
|
363
|
+
# cannot contain characters that are not \d
|
364
|
+
if CONTENT_LENGTH_VALUE_INVALID.match? cl
|
365
|
+
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
366
|
+
end
|
367
|
+
else
|
305
368
|
@buffer = body.empty? ? nil : body
|
306
369
|
@body = EmptyBody
|
307
370
|
set_ready
|
@@ -319,6 +382,7 @@ module Puma
|
|
319
382
|
|
320
383
|
if remain > MAX_BODY
|
321
384
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
385
|
+
@body.unlink
|
322
386
|
@body.binmode
|
323
387
|
@tempfile = @body
|
324
388
|
else
|
@@ -331,7 +395,7 @@ module Puma
|
|
331
395
|
|
332
396
|
@body_remain = remain
|
333
397
|
|
334
|
-
|
398
|
+
false
|
335
399
|
end
|
336
400
|
|
337
401
|
def read_body
|
@@ -351,7 +415,7 @@ module Puma
|
|
351
415
|
|
352
416
|
begin
|
353
417
|
chunk = @io.read_nonblock(want)
|
354
|
-
rescue
|
418
|
+
rescue IO::WaitReadable
|
355
419
|
return false
|
356
420
|
rescue SystemCallError, IOError
|
357
421
|
raise ConnectionError, "Connection error detected during read"
|
@@ -397,7 +461,10 @@ module Puma
|
|
397
461
|
raise EOFError
|
398
462
|
end
|
399
463
|
|
400
|
-
|
464
|
+
if decode_chunk(chunk)
|
465
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
466
|
+
return true
|
467
|
+
end
|
401
468
|
end
|
402
469
|
end
|
403
470
|
|
@@ -407,22 +474,40 @@ module Puma
|
|
407
474
|
@prev_chunk = ""
|
408
475
|
|
409
476
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
477
|
+
@body.unlink
|
410
478
|
@body.binmode
|
411
479
|
@tempfile = @body
|
480
|
+
@chunked_content_length = 0
|
412
481
|
|
413
|
-
|
482
|
+
if decode_chunk(body)
|
483
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
484
|
+
return true
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# @version 5.0.0
|
489
|
+
def write_chunk(str)
|
490
|
+
@chunked_content_length += @body.write(str)
|
414
491
|
end
|
415
492
|
|
416
493
|
def decode_chunk(chunk)
|
417
494
|
if @partial_part_left > 0
|
418
495
|
if @partial_part_left <= chunk.size
|
419
496
|
if @partial_part_left > 2
|
420
|
-
|
497
|
+
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
|
421
498
|
end
|
422
499
|
chunk = chunk[@partial_part_left..-1]
|
423
500
|
@partial_part_left = 0
|
424
501
|
else
|
425
|
-
|
502
|
+
if @partial_part_left > 2
|
503
|
+
if @partial_part_left == chunk.size + 1
|
504
|
+
# Don't include the last \r
|
505
|
+
write_chunk(chunk[0..(@partial_part_left-3)])
|
506
|
+
else
|
507
|
+
# don't include the last \r\n
|
508
|
+
write_chunk(chunk)
|
509
|
+
end
|
510
|
+
end
|
426
511
|
@partial_part_left -= chunk.size
|
427
512
|
return false
|
428
513
|
end
|
@@ -438,7 +523,13 @@ module Puma
|
|
438
523
|
while !io.eof?
|
439
524
|
line = io.gets
|
440
525
|
if line.end_with?("\r\n")
|
441
|
-
|
526
|
+
# Puma doesn't process chunk extensions, but should parse if they're
|
527
|
+
# present, which is the reason for the semicolon regex
|
528
|
+
chunk_hex = line.strip[/\A[^;]+/]
|
529
|
+
if CHUNK_SIZE_INVALID.match? chunk_hex
|
530
|
+
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
531
|
+
end
|
532
|
+
len = chunk_hex.to_i(16)
|
442
533
|
if len == 0
|
443
534
|
@in_last_chunk = true
|
444
535
|
@body.rewind
|
@@ -469,12 +560,17 @@ module Puma
|
|
469
560
|
|
470
561
|
case
|
471
562
|
when got == len
|
472
|
-
|
563
|
+
# proper chunked segment must end with "\r\n"
|
564
|
+
if part.end_with? CHUNK_VALID_ENDING
|
565
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
566
|
+
else
|
567
|
+
raise HttpParserError, "Chunk size mismatch"
|
568
|
+
end
|
473
569
|
when got <= len - 2
|
474
|
-
|
570
|
+
write_chunk(part)
|
475
571
|
@partial_part_left = len - part.size
|
476
572
|
when got == len - 1 # edge where we get just \r but not \n
|
477
|
-
|
573
|
+
write_chunk(part[0..-2])
|
478
574
|
@partial_part_left = len - part.size
|
479
575
|
end
|
480
576
|
else
|
@@ -493,7 +589,7 @@ module Puma
|
|
493
589
|
|
494
590
|
def set_ready
|
495
591
|
if @body_read_start
|
496
|
-
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
592
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
|
497
593
|
end
|
498
594
|
@requests_served += 1
|
499
595
|
@ready = true
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puma
|
4
|
+
class Cluster < Puma::Runner
|
5
|
+
#—————————————————————— DO NOT USE — this class is for internal use only ———
|
6
|
+
|
7
|
+
|
8
|
+
# This class is instantiated by the `Puma::Cluster` and represents a single
|
9
|
+
# worker process.
|
10
|
+
#
|
11
|
+
# At the core of this class is running an instance of `Puma::Server` which
|
12
|
+
# gets created via the `start_server` method from the `Puma::Runner` class
|
13
|
+
# that this inherits from.
|
14
|
+
class Worker < Puma::Runner # :nodoc:
|
15
|
+
attr_reader :index, :master
|
16
|
+
|
17
|
+
def initialize(index:, master:, launcher:, pipes:, server: nil)
|
18
|
+
super(launcher)
|
19
|
+
|
20
|
+
@index = index
|
21
|
+
@master = master
|
22
|
+
@check_pipe = pipes[:check_pipe]
|
23
|
+
@worker_write = pipes[:worker_write]
|
24
|
+
@fork_pipe = pipes[:fork_pipe]
|
25
|
+
@wakeup = pipes[:wakeup]
|
26
|
+
@server = server
|
27
|
+
@hook_data = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
title = "puma: cluster worker #{index}: #{master}"
|
32
|
+
title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
|
33
|
+
$0 = title
|
34
|
+
|
35
|
+
Signal.trap "SIGINT", "IGNORE"
|
36
|
+
Signal.trap "SIGCHLD", "DEFAULT"
|
37
|
+
|
38
|
+
Thread.new do
|
39
|
+
Puma.set_thread_name "wrkr check"
|
40
|
+
@check_pipe.wait_readable
|
41
|
+
log "! Detected parent died, dying"
|
42
|
+
exit! 1
|
43
|
+
end
|
44
|
+
|
45
|
+
# If we're not running under a Bundler context, then
|
46
|
+
# report the info about the context we will be using
|
47
|
+
if !ENV['BUNDLE_GEMFILE']
|
48
|
+
if File.exist?("Gemfile")
|
49
|
+
log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
|
50
|
+
elsif File.exist?("gems.rb")
|
51
|
+
log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Invoke any worker boot hooks so they can get
|
56
|
+
# things in shape before booting the app.
|
57
|
+
@config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
|
58
|
+
|
59
|
+
begin
|
60
|
+
server = @server ||= start_server
|
61
|
+
rescue Exception => e
|
62
|
+
log "! Unable to start worker"
|
63
|
+
log e
|
64
|
+
log e.backtrace.join("\n ")
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
|
68
|
+
restart_server = Queue.new << true << false
|
69
|
+
|
70
|
+
fork_worker = @options[:fork_worker] && index == 0
|
71
|
+
|
72
|
+
if fork_worker
|
73
|
+
restart_server.clear
|
74
|
+
worker_pids = []
|
75
|
+
Signal.trap "SIGCHLD" do
|
76
|
+
wakeup! if worker_pids.reject! do |p|
|
77
|
+
Process.wait(p, Process::WNOHANG) rescue true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
Thread.new do
|
82
|
+
Puma.set_thread_name "wrkr fork"
|
83
|
+
while (idx = @fork_pipe.gets)
|
84
|
+
idx = idx.to_i
|
85
|
+
if idx == -1 # stop server
|
86
|
+
if restart_server.length > 0
|
87
|
+
restart_server.clear
|
88
|
+
server.begin_restart(true)
|
89
|
+
@config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
|
90
|
+
end
|
91
|
+
elsif idx == 0 # restart server
|
92
|
+
restart_server << true << false
|
93
|
+
else # fork worker
|
94
|
+
worker_pids << pid = spawn_worker(idx)
|
95
|
+
@worker_write << "f#{pid}:#{idx}\n" rescue nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Signal.trap "SIGTERM" do
|
102
|
+
@worker_write << "e#{Process.pid}\n" rescue nil
|
103
|
+
restart_server.clear
|
104
|
+
server.stop
|
105
|
+
restart_server << false
|
106
|
+
end
|
107
|
+
|
108
|
+
begin
|
109
|
+
@worker_write << "b#{Process.pid}:#{index}\n"
|
110
|
+
rescue SystemCallError, IOError
|
111
|
+
Puma::Util.purge_interrupt_queue
|
112
|
+
STDERR.puts "Master seems to have exited, exiting."
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
while restart_server.pop
|
117
|
+
server_thread = server.run
|
118
|
+
stat_thread ||= Thread.new(@worker_write) do |io|
|
119
|
+
Puma.set_thread_name "stat pld"
|
120
|
+
base_payload = "p#{Process.pid}"
|
121
|
+
|
122
|
+
while true
|
123
|
+
begin
|
124
|
+
b = server.backlog || 0
|
125
|
+
r = server.running || 0
|
126
|
+
t = server.pool_capacity || 0
|
127
|
+
m = server.max_threads || 0
|
128
|
+
rc = server.requests_count || 0
|
129
|
+
payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
|
130
|
+
io << payload
|
131
|
+
rescue IOError
|
132
|
+
Puma::Util.purge_interrupt_queue
|
133
|
+
break
|
134
|
+
end
|
135
|
+
sleep @options[:worker_check_interval]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
server_thread.join
|
139
|
+
end
|
140
|
+
|
141
|
+
# Invoke any worker shutdown hooks so they can prevent the worker
|
142
|
+
# exiting until any background operations are completed
|
143
|
+
@config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
|
144
|
+
ensure
|
145
|
+
@worker_write << "t#{Process.pid}\n" rescue nil
|
146
|
+
@worker_write.close
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def spawn_worker(idx)
|
152
|
+
@config.run_hooks(:before_worker_fork, idx, @log_writer, @hook_data)
|
153
|
+
|
154
|
+
pid = fork do
|
155
|
+
new_worker = Worker.new index: idx,
|
156
|
+
master: master,
|
157
|
+
launcher: @launcher,
|
158
|
+
pipes: { check_pipe: @check_pipe,
|
159
|
+
worker_write: @worker_write },
|
160
|
+
server: @server
|
161
|
+
new_worker.run
|
162
|
+
end
|
163
|
+
|
164
|
+
if !pid
|
165
|
+
log "! Complete inability to spawn new workers detected"
|
166
|
+
log "! Seppuku is the only choice."
|
167
|
+
exit! 1
|
168
|
+
end
|
169
|
+
|
170
|
+
@config.run_hooks(:after_worker_fork, idx, @log_writer, @hook_data)
|
171
|
+
pid
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|