puma 4.3.12 → 6.0.0
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 +1591 -521
- 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/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/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 +49 -12
- data/ext/puma_http11/http11_parser.c +46 -48
- 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 +2 -2
- data/ext/puma_http11/mini_ssl.c +250 -93
- 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 +4 -6
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
- data/ext/puma_http11/puma_http11.c +46 -57
- data/lib/puma/app/status.rb +52 -38
- data/lib/puma/binder.rb +232 -119
- data/lib/puma/cli.rb +33 -33
- data/lib/puma/client.rb +125 -87
- 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 +25 -22
- data/lib/puma/control_cli.rb +99 -79
- data/lib/puma/detect.rb +31 -2
- data/lib/puma/dsl.rb +423 -110
- data/lib/puma/error_logger.rb +112 -0
- data/lib/puma/events.rb +16 -115
- data/lib/puma/io_buffer.rb +34 -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_default.rb +1 -1
- data/lib/puma/reactor.rb +85 -369
- data/lib/puma/request.rb +607 -0
- data/lib/puma/runner.rb +83 -77
- data/lib/puma/server.rb +305 -789
- 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 +137 -66
- 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
- metadata +31 -23
- 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,7 @@ class IO
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
require_relative 'detect'
|
12
12
|
require 'tempfile'
|
13
13
|
require 'forwardable'
|
14
14
|
|
@@ -25,6 +25,9 @@ module Puma
|
|
25
25
|
|
26
26
|
class HttpParserError501 < IOError; end
|
27
27
|
|
28
|
+
#———————————————————————— DO NOT USE — this class is for internal use only ———
|
29
|
+
|
30
|
+
|
28
31
|
# An instance of this class represents a unique request from a client.
|
29
32
|
# For example, this could be a web request from a browser or from CURL.
|
30
33
|
#
|
@@ -38,7 +41,7 @@ module Puma
|
|
38
41
|
# the header and body are fully buffered via the `try_to_finish` method.
|
39
42
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
40
43
|
#
|
41
|
-
class Client
|
44
|
+
class Client # :nodoc:
|
42
45
|
|
43
46
|
# this tests all values but the last, which must be chunked
|
44
47
|
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
@@ -63,15 +66,12 @@ module Puma
|
|
63
66
|
@io = io
|
64
67
|
@to_io = io.to_io
|
65
68
|
@proto_env = env
|
66
|
-
|
67
|
-
@env = nil
|
68
|
-
else
|
69
|
-
@env = env.dup
|
70
|
-
end
|
69
|
+
@env = env ? env.dup : nil
|
71
70
|
|
72
71
|
@parser = HttpParser.new
|
73
72
|
@parsed_bytes = 0
|
74
73
|
@read_header = true
|
74
|
+
@read_proxy = false
|
75
75
|
@ready = false
|
76
76
|
|
77
77
|
@body = nil
|
@@ -85,7 +85,10 @@ module Puma
|
|
85
85
|
@hijacked = false
|
86
86
|
|
87
87
|
@peerip = nil
|
88
|
+
@peer_family = nil
|
89
|
+
@listener = nil
|
88
90
|
@remote_addr_header = nil
|
91
|
+
@expect_proxy_proto = false
|
89
92
|
|
90
93
|
@body_remain = 0
|
91
94
|
|
@@ -97,10 +100,17 @@ module Puma
|
|
97
100
|
|
98
101
|
attr_writer :peerip
|
99
102
|
|
100
|
-
attr_accessor :remote_addr_header
|
103
|
+
attr_accessor :remote_addr_header, :listener
|
101
104
|
|
102
105
|
def_delegators :@io, :closed?
|
103
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
|
104
114
|
def inspect
|
105
115
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
106
116
|
end
|
@@ -112,27 +122,36 @@ module Puma
|
|
112
122
|
env[HIJACK_IO] ||= @io
|
113
123
|
end
|
114
124
|
|
125
|
+
# @!attribute [r] in_data_phase
|
115
126
|
def in_data_phase
|
116
|
-
|
127
|
+
!(@read_header || @read_proxy)
|
117
128
|
end
|
118
129
|
|
119
130
|
def set_timeout(val)
|
120
|
-
@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
|
121
137
|
end
|
122
138
|
|
123
139
|
def reset(fast_check=true)
|
124
140
|
@parser.reset
|
125
141
|
@read_header = true
|
142
|
+
@read_proxy = !!@expect_proxy_proto
|
126
143
|
@env = @proto_env.dup
|
127
144
|
@body = nil
|
128
145
|
@tempfile = nil
|
129
146
|
@parsed_bytes = 0
|
130
147
|
@ready = false
|
131
148
|
@body_remain = 0
|
132
|
-
@peerip = nil
|
149
|
+
@peerip = nil if @remote_addr_header
|
133
150
|
@in_last_chunk = false
|
134
151
|
|
135
152
|
if @buffer
|
153
|
+
return false unless try_to_parse_proxy_protocol
|
154
|
+
|
136
155
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
137
156
|
|
138
157
|
if @parser.finished?
|
@@ -145,8 +164,7 @@ module Puma
|
|
145
164
|
return false
|
146
165
|
else
|
147
166
|
begin
|
148
|
-
if fast_check &&
|
149
|
-
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
167
|
+
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
150
168
|
return try_to_finish
|
151
169
|
end
|
152
170
|
rescue IOError
|
@@ -159,19 +177,45 @@ module Puma
|
|
159
177
|
def close
|
160
178
|
begin
|
161
179
|
@io.close
|
162
|
-
rescue IOError
|
163
|
-
|
180
|
+
rescue IOError, Errno::EBADF
|
181
|
+
Puma::Util.purge_interrupt_queue
|
164
182
|
end
|
165
183
|
end
|
166
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
|
205
|
+
end
|
206
|
+
true
|
207
|
+
end
|
208
|
+
|
167
209
|
def try_to_finish
|
168
|
-
return read_body
|
210
|
+
return read_body if in_data_phase
|
169
211
|
|
170
212
|
begin
|
171
213
|
data = @io.read_nonblock(CHUNK_SIZE)
|
172
214
|
rescue IO::WaitReadable
|
173
215
|
return false
|
174
|
-
rescue
|
216
|
+
rescue EOFError
|
217
|
+
# Swallow error, don't log
|
218
|
+
rescue SystemCallError, IOError
|
175
219
|
raise ConnectionError, "Connection error detected during read"
|
176
220
|
end
|
177
221
|
|
@@ -188,6 +232,8 @@ module Puma
|
|
188
232
|
@buffer = data
|
189
233
|
end
|
190
234
|
|
235
|
+
return false unless try_to_parse_proxy_protocol
|
236
|
+
|
191
237
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
192
238
|
|
193
239
|
if @parser.finished?
|
@@ -200,68 +246,20 @@ module Puma
|
|
200
246
|
false
|
201
247
|
end
|
202
248
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
data = @io.sysread_nonblock(CHUNK_SIZE)
|
209
|
-
rescue OpenSSL::SSL::SSLError => e
|
210
|
-
return false if e.kind_of? IO::WaitReadable
|
211
|
-
raise e
|
212
|
-
end
|
213
|
-
|
214
|
-
# No data means a closed socket
|
215
|
-
unless data
|
216
|
-
@buffer = nil
|
217
|
-
set_ready
|
218
|
-
raise EOFError
|
219
|
-
end
|
220
|
-
|
221
|
-
if @buffer
|
222
|
-
@buffer << data
|
223
|
-
else
|
224
|
-
@buffer = data
|
225
|
-
end
|
226
|
-
|
227
|
-
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
228
|
-
|
229
|
-
if @parser.finished?
|
230
|
-
return setup_body
|
231
|
-
elsif @parsed_bytes >= MAX_HEADER
|
232
|
-
raise HttpParserError,
|
233
|
-
"HEADER is longer than allowed, aborting client early."
|
234
|
-
end
|
235
|
-
|
236
|
-
false
|
237
|
-
end
|
238
|
-
|
239
|
-
def eagerly_finish
|
240
|
-
return true if @ready
|
241
|
-
|
242
|
-
if @io.kind_of? OpenSSL::SSL::SSLSocket
|
243
|
-
return true if jruby_start_try_to_finish
|
244
|
-
end
|
245
|
-
|
246
|
-
return false unless IO.select([@to_io], nil, nil, 0)
|
247
|
-
try_to_finish
|
248
|
-
end
|
249
|
-
|
250
|
-
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
|
251
254
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
end
|
257
|
-
end # IS_JRUBY
|
255
|
+
def finish(timeout)
|
256
|
+
return if @ready
|
257
|
+
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
258
|
+
end
|
258
259
|
|
259
|
-
def
|
260
|
-
|
261
|
-
|
262
|
-
IO.select([@to_io], nil, nil)
|
263
|
-
end
|
264
|
-
true
|
260
|
+
def timeout!
|
261
|
+
write_error(408) if in_data_phase
|
262
|
+
raise ConnectionError
|
265
263
|
end
|
266
264
|
|
267
265
|
def write_error(status_code)
|
@@ -275,7 +273,7 @@ module Puma
|
|
275
273
|
return @peerip if @peerip
|
276
274
|
|
277
275
|
if @remote_addr_header
|
278
|
-
hdr = (@env[@remote_addr_header] ||
|
276
|
+
hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
|
279
277
|
@peerip = hdr
|
280
278
|
return hdr
|
281
279
|
end
|
@@ -283,10 +281,40 @@ module Puma
|
|
283
281
|
@peerip ||= @io.peeraddr.last
|
284
282
|
end
|
285
283
|
|
284
|
+
def peer_family
|
285
|
+
return @peer_family if @peer_family
|
286
|
+
|
287
|
+
@peer_family ||= begin
|
288
|
+
@io.local_address.afamily
|
289
|
+
rescue
|
290
|
+
Socket::AF_INET
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Returns true if the persistent connection can be closed immediately
|
295
|
+
# without waiting for the configured idle/shutdown timeout.
|
296
|
+
# @version 5.0.0
|
297
|
+
#
|
298
|
+
def can_close?
|
299
|
+
# Allow connection to close if we're not in the middle of parsing a request.
|
300
|
+
@parsed_bytes == 0
|
301
|
+
end
|
302
|
+
|
303
|
+
def expect_proxy_proto=(val)
|
304
|
+
if val
|
305
|
+
if @read_header
|
306
|
+
@read_proxy = true
|
307
|
+
end
|
308
|
+
else
|
309
|
+
@read_proxy = false
|
310
|
+
end
|
311
|
+
@expect_proxy_proto = val
|
312
|
+
end
|
313
|
+
|
286
314
|
private
|
287
315
|
|
288
316
|
def setup_body
|
289
|
-
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
317
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
290
318
|
|
291
319
|
if @env[HTTP_EXPECT] == CONTINUE
|
292
320
|
# TODO allow a hook here to check the headers before
|
@@ -330,7 +358,7 @@ module Puma
|
|
330
358
|
|
331
359
|
if cl
|
332
360
|
# cannot contain characters that are not \d
|
333
|
-
if cl
|
361
|
+
if CONTENT_LENGTH_VALUE_INVALID.match? cl
|
334
362
|
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
335
363
|
end
|
336
364
|
else
|
@@ -351,6 +379,7 @@ module Puma
|
|
351
379
|
|
352
380
|
if remain > MAX_BODY
|
353
381
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
382
|
+
@body.unlink
|
354
383
|
@body.binmode
|
355
384
|
@tempfile = @body
|
356
385
|
else
|
@@ -363,7 +392,7 @@ module Puma
|
|
363
392
|
|
364
393
|
@body_remain = remain
|
365
394
|
|
366
|
-
|
395
|
+
false
|
367
396
|
end
|
368
397
|
|
369
398
|
def read_body
|
@@ -430,7 +459,7 @@ module Puma
|
|
430
459
|
end
|
431
460
|
|
432
461
|
if decode_chunk(chunk)
|
433
|
-
@env[CONTENT_LENGTH] = @chunked_content_length
|
462
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
434
463
|
return true
|
435
464
|
end
|
436
465
|
end
|
@@ -442,17 +471,18 @@ module Puma
|
|
442
471
|
@prev_chunk = ""
|
443
472
|
|
444
473
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
474
|
+
@body.unlink
|
445
475
|
@body.binmode
|
446
476
|
@tempfile = @body
|
447
|
-
|
448
477
|
@chunked_content_length = 0
|
449
478
|
|
450
479
|
if decode_chunk(body)
|
451
|
-
@env[CONTENT_LENGTH] = @chunked_content_length
|
480
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
452
481
|
return true
|
453
482
|
end
|
454
483
|
end
|
455
484
|
|
485
|
+
# @version 5.0.0
|
456
486
|
def write_chunk(str)
|
457
487
|
@chunked_content_length += @body.write(str)
|
458
488
|
end
|
@@ -466,7 +496,15 @@ module Puma
|
|
466
496
|
chunk = chunk[@partial_part_left..-1]
|
467
497
|
@partial_part_left = 0
|
468
498
|
else
|
469
|
-
|
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
|
470
508
|
@partial_part_left -= chunk.size
|
471
509
|
return false
|
472
510
|
end
|
@@ -485,7 +523,7 @@ module Puma
|
|
485
523
|
# Puma doesn't process chunk extensions, but should parse if they're
|
486
524
|
# present, which is the reason for the semicolon regex
|
487
525
|
chunk_hex = line.strip[/\A[^;]+/]
|
488
|
-
if chunk_hex
|
526
|
+
if CHUNK_SIZE_INVALID.match? chunk_hex
|
489
527
|
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
490
528
|
end
|
491
529
|
len = chunk_hex.to_i(16)
|
@@ -548,7 +586,7 @@ module Puma
|
|
548
586
|
|
549
587
|
def set_ready
|
550
588
|
if @body_read_start
|
551
|
-
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
589
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
|
552
590
|
end
|
553
591
|
@requests_served += 1
|
554
592
|
@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
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puma
|
4
|
+
class Cluster < Runner
|
5
|
+
#—————————————————————— DO NOT USE — this class is for internal use only ———
|
6
|
+
|
7
|
+
|
8
|
+
# This class represents a worker process from the perspective of the puma
|
9
|
+
# master process. It contains information about the process and its health
|
10
|
+
# and it exposes methods to control the process via IPC. It does not
|
11
|
+
# include the actual logic executed by the worker process itself. For that,
|
12
|
+
# see Puma::Cluster::Worker.
|
13
|
+
class WorkerHandle # :nodoc:
|
14
|
+
def initialize(idx, pid, phase, options)
|
15
|
+
@index = idx
|
16
|
+
@pid = pid
|
17
|
+
@phase = phase
|
18
|
+
@stage = :started
|
19
|
+
@signal = "TERM"
|
20
|
+
@options = options
|
21
|
+
@first_term_sent = nil
|
22
|
+
@started_at = Time.now
|
23
|
+
@last_checkin = Time.now
|
24
|
+
@last_status = {}
|
25
|
+
@term = false
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
29
|
+
|
30
|
+
# @version 5.0.0
|
31
|
+
attr_writer :pid, :phase
|
32
|
+
|
33
|
+
def booted?
|
34
|
+
@stage == :booted
|
35
|
+
end
|
36
|
+
|
37
|
+
def uptime
|
38
|
+
Time.now - started_at
|
39
|
+
end
|
40
|
+
|
41
|
+
def boot!
|
42
|
+
@last_checkin = Time.now
|
43
|
+
@stage = :booted
|
44
|
+
end
|
45
|
+
|
46
|
+
def term!
|
47
|
+
@term = true
|
48
|
+
end
|
49
|
+
|
50
|
+
def term?
|
51
|
+
@term
|
52
|
+
end
|
53
|
+
|
54
|
+
def ping!(status)
|
55
|
+
@last_checkin = Time.now
|
56
|
+
captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
|
57
|
+
@last_status = captures.names.inject({}) do |hash, key|
|
58
|
+
hash[key.to_sym] = captures[key].to_i
|
59
|
+
hash
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @see Puma::Cluster#check_workers
|
64
|
+
# @version 5.0.0
|
65
|
+
def ping_timeout
|
66
|
+
@last_checkin +
|
67
|
+
(booted? ?
|
68
|
+
@options[:worker_timeout] :
|
69
|
+
@options[:worker_boot_timeout]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def term
|
74
|
+
begin
|
75
|
+
if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
|
76
|
+
@signal = "KILL"
|
77
|
+
else
|
78
|
+
@term ||= true
|
79
|
+
@first_term_sent ||= Time.now
|
80
|
+
end
|
81
|
+
Process.kill @signal, @pid if @pid
|
82
|
+
rescue Errno::ESRCH
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def kill
|
87
|
+
@signal = 'KILL'
|
88
|
+
term
|
89
|
+
end
|
90
|
+
|
91
|
+
def hup
|
92
|
+
Process.kill "HUP", @pid
|
93
|
+
rescue Errno::ESRCH
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|