puma 4.3.6 → 5.6.4
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 +1486 -518
- 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/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 +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 +46 -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 +275 -122
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +51 -51
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +105 -61
- data/ext/puma_http11/puma_http11.c +32 -51
- data/lib/puma/app/status.rb +47 -36
- data/lib/puma/binder.rb +225 -106
- data/lib/puma/cli.rb +24 -18
- data/lib/puma/client.rb +174 -91
- 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 +18 -9
- data/lib/puma/control_cli.rb +93 -76
- data/lib/puma/detect.rb +29 -2
- data/lib/puma/dsl.rb +364 -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 +117 -46
- data/lib/puma/minissl/context_builder.rb +14 -9
- data/lib/puma/minissl.rb +128 -46
- data/lib/puma/null_io.rb +13 -1
- data/lib/puma/plugin/tmp_restart.rb +0 -0
- 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/rack/urlmap.rb +0 -0
- data/lib/puma/rack_default.rb +0 -0
- data/lib/puma/reactor.rb +85 -369
- data/lib/puma/request.rb +472 -0
- data/lib/puma/runner.rb +46 -61
- data/lib/puma/server.rb +287 -743
- data/lib/puma/single.rb +9 -65
- data/lib/puma/state_file.rb +47 -8
- data/lib/puma/systemd.rb +46 -0
- data/lib/puma/thread_pool.rb +125 -57
- data/lib/puma/util.rb +20 -1
- data/lib/puma.rb +46 -0
- data/lib/rack/handler/puma.rb +2 -3
- data/tools/{docker/Dockerfile → Dockerfile} +1 -1
- data/tools/trickletest.rb +0 -0
- metadata +28 -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/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,10 +100,17 @@ 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
|
|
107
|
+
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
108
|
+
# used for MiniSSL::Socket
|
109
|
+
def io_ok?
|
110
|
+
@to_io.is_a?(::BasicSocket) && !closed?
|
111
|
+
end
|
112
|
+
|
113
|
+
# @!attribute [r] inspect
|
88
114
|
def inspect
|
89
115
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
90
116
|
end
|
@@ -96,27 +122,36 @@ module Puma
|
|
96
122
|
env[HIJACK_IO] ||= @io
|
97
123
|
end
|
98
124
|
|
125
|
+
# @!attribute [r] in_data_phase
|
99
126
|
def in_data_phase
|
100
|
-
|
127
|
+
!(@read_header || @read_proxy)
|
101
128
|
end
|
102
129
|
|
103
130
|
def set_timeout(val)
|
104
|
-
@timeout_at =
|
131
|
+
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
|
132
|
+
end
|
133
|
+
|
134
|
+
# Number of seconds until the timeout elapses.
|
135
|
+
def timeout
|
136
|
+
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
105
137
|
end
|
106
138
|
|
107
139
|
def reset(fast_check=true)
|
108
140
|
@parser.reset
|
109
141
|
@read_header = true
|
142
|
+
@read_proxy = !!@expect_proxy_proto
|
110
143
|
@env = @proto_env.dup
|
111
144
|
@body = nil
|
112
145
|
@tempfile = nil
|
113
146
|
@parsed_bytes = 0
|
114
147
|
@ready = false
|
115
148
|
@body_remain = 0
|
116
|
-
@peerip = nil
|
149
|
+
@peerip = nil if @remote_addr_header
|
117
150
|
@in_last_chunk = false
|
118
151
|
|
119
152
|
if @buffer
|
153
|
+
return false unless try_to_parse_proxy_protocol
|
154
|
+
|
120
155
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
121
156
|
|
122
157
|
if @parser.finished?
|
@@ -129,8 +164,7 @@ module Puma
|
|
129
164
|
return false
|
130
165
|
else
|
131
166
|
begin
|
132
|
-
if fast_check &&
|
133
|
-
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
167
|
+
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
134
168
|
return try_to_finish
|
135
169
|
end
|
136
170
|
rescue IOError
|
@@ -143,19 +177,45 @@ module Puma
|
|
143
177
|
def close
|
144
178
|
begin
|
145
179
|
@io.close
|
146
|
-
rescue IOError
|
147
|
-
|
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
|
148
205
|
end
|
206
|
+
true
|
149
207
|
end
|
150
208
|
|
151
209
|
def try_to_finish
|
152
|
-
return read_body
|
210
|
+
return read_body if in_data_phase
|
153
211
|
|
154
212
|
begin
|
155
213
|
data = @io.read_nonblock(CHUNK_SIZE)
|
156
|
-
rescue
|
214
|
+
rescue IO::WaitReadable
|
157
215
|
return false
|
158
|
-
rescue
|
216
|
+
rescue EOFError
|
217
|
+
# Swallow error, don't log
|
218
|
+
rescue SystemCallError, IOError
|
159
219
|
raise ConnectionError, "Connection error detected during read"
|
160
220
|
end
|
161
221
|
|
@@ -172,6 +232,8 @@ module Puma
|
|
172
232
|
@buffer = data
|
173
233
|
end
|
174
234
|
|
235
|
+
return false unless try_to_parse_proxy_protocol
|
236
|
+
|
175
237
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
176
238
|
|
177
239
|
if @parser.finished?
|
@@ -184,68 +246,20 @@ module Puma
|
|
184
246
|
false
|
185
247
|
end
|
186
248
|
|
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
|
249
|
+
def eagerly_finish
|
250
|
+
return true if @ready
|
251
|
+
return false unless @to_io.wait_readable(0)
|
252
|
+
try_to_finish
|
253
|
+
end
|
235
254
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
end
|
241
|
-
end # IS_JRUBY
|
255
|
+
def finish(timeout)
|
256
|
+
return if @ready
|
257
|
+
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
258
|
+
end
|
242
259
|
|
243
|
-
def
|
244
|
-
|
245
|
-
|
246
|
-
IO.select([@to_io], nil, nil)
|
247
|
-
end
|
248
|
-
true
|
260
|
+
def timeout!
|
261
|
+
write_error(408) if in_data_phase
|
262
|
+
raise ConnectionError
|
249
263
|
end
|
250
264
|
|
251
265
|
def write_error(status_code)
|
@@ -259,7 +273,7 @@ module Puma
|
|
259
273
|
return @peerip if @peerip
|
260
274
|
|
261
275
|
if @remote_addr_header
|
262
|
-
hdr = (@env[@remote_addr_header] ||
|
276
|
+
hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
|
263
277
|
@peerip = hdr
|
264
278
|
return hdr
|
265
279
|
end
|
@@ -267,6 +281,26 @@ module Puma
|
|
267
281
|
@peerip ||= @io.peeraddr.last
|
268
282
|
end
|
269
283
|
|
284
|
+
# Returns true if the persistent connection can be closed immediately
|
285
|
+
# without waiting for the configured idle/shutdown timeout.
|
286
|
+
# @version 5.0.0
|
287
|
+
#
|
288
|
+
def can_close?
|
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
|
302
|
+
end
|
303
|
+
|
270
304
|
private
|
271
305
|
|
272
306
|
def setup_body
|
@@ -284,16 +318,27 @@ module Puma
|
|
284
318
|
body = @parser.body
|
285
319
|
|
286
320
|
te = @env[TRANSFER_ENCODING2]
|
287
|
-
|
288
321
|
if te
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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}'"
|
294
334
|
end
|
295
|
-
elsif
|
296
|
-
|
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}'"
|
297
342
|
end
|
298
343
|
end
|
299
344
|
|
@@ -301,7 +346,12 @@ module Puma
|
|
301
346
|
|
302
347
|
cl = @env[CONTENT_LENGTH]
|
303
348
|
|
304
|
-
|
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
|
305
355
|
@buffer = body.empty? ? nil : body
|
306
356
|
@body = EmptyBody
|
307
357
|
set_ready
|
@@ -319,6 +369,7 @@ module Puma
|
|
319
369
|
|
320
370
|
if remain > MAX_BODY
|
321
371
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
372
|
+
@body.unlink
|
322
373
|
@body.binmode
|
323
374
|
@tempfile = @body
|
324
375
|
else
|
@@ -331,7 +382,7 @@ module Puma
|
|
331
382
|
|
332
383
|
@body_remain = remain
|
333
384
|
|
334
|
-
|
385
|
+
false
|
335
386
|
end
|
336
387
|
|
337
388
|
def read_body
|
@@ -351,7 +402,7 @@ module Puma
|
|
351
402
|
|
352
403
|
begin
|
353
404
|
chunk = @io.read_nonblock(want)
|
354
|
-
rescue
|
405
|
+
rescue IO::WaitReadable
|
355
406
|
return false
|
356
407
|
rescue SystemCallError, IOError
|
357
408
|
raise ConnectionError, "Connection error detected during read"
|
@@ -397,7 +448,10 @@ module Puma
|
|
397
448
|
raise EOFError
|
398
449
|
end
|
399
450
|
|
400
|
-
|
451
|
+
if decode_chunk(chunk)
|
452
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
453
|
+
return true
|
454
|
+
end
|
401
455
|
end
|
402
456
|
end
|
403
457
|
|
@@ -407,22 +461,40 @@ module Puma
|
|
407
461
|
@prev_chunk = ""
|
408
462
|
|
409
463
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
464
|
+
@body.unlink
|
410
465
|
@body.binmode
|
411
466
|
@tempfile = @body
|
467
|
+
@chunked_content_length = 0
|
468
|
+
|
469
|
+
if decode_chunk(body)
|
470
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
471
|
+
return true
|
472
|
+
end
|
473
|
+
end
|
412
474
|
|
413
|
-
|
475
|
+
# @version 5.0.0
|
476
|
+
def write_chunk(str)
|
477
|
+
@chunked_content_length += @body.write(str)
|
414
478
|
end
|
415
479
|
|
416
480
|
def decode_chunk(chunk)
|
417
481
|
if @partial_part_left > 0
|
418
482
|
if @partial_part_left <= chunk.size
|
419
483
|
if @partial_part_left > 2
|
420
|
-
|
484
|
+
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
|
421
485
|
end
|
422
486
|
chunk = chunk[@partial_part_left..-1]
|
423
487
|
@partial_part_left = 0
|
424
488
|
else
|
425
|
-
|
489
|
+
if @partial_part_left > 2
|
490
|
+
if @partial_part_left == chunk.size + 1
|
491
|
+
# Don't include the last \r
|
492
|
+
write_chunk(chunk[0..(@partial_part_left-3)])
|
493
|
+
else
|
494
|
+
# don't include the last \r\n
|
495
|
+
write_chunk(chunk)
|
496
|
+
end
|
497
|
+
end
|
426
498
|
@partial_part_left -= chunk.size
|
427
499
|
return false
|
428
500
|
end
|
@@ -438,7 +510,13 @@ module Puma
|
|
438
510
|
while !io.eof?
|
439
511
|
line = io.gets
|
440
512
|
if line.end_with?("\r\n")
|
441
|
-
|
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)
|
442
520
|
if len == 0
|
443
521
|
@in_last_chunk = true
|
444
522
|
@body.rewind
|
@@ -469,12 +547,17 @@ module Puma
|
|
469
547
|
|
470
548
|
case
|
471
549
|
when got == len
|
472
|
-
|
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
|
473
556
|
when got <= len - 2
|
474
|
-
|
557
|
+
write_chunk(part)
|
475
558
|
@partial_part_left = len - part.size
|
476
559
|
when got == len - 1 # edge where we get just \r but not \n
|
477
|
-
|
560
|
+
write_chunk(part[0..-2])
|
478
561
|
@partial_part_left = len - part.size
|
479
562
|
end
|
480
563
|
else
|
@@ -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
|