puma 4.3.3 → 5.3.2
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 +1348 -519
- data/LICENSE +23 -20
- data/README.md +74 -31
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +24 -20
- data/docs/compile_options.md +19 -0
- data/docs/deployment.md +15 -10
- 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/{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 +2 -2
- data/docs/rails_dev_mode.md +29 -0
- data/docs/restart.md +46 -23
- data/docs/signals.md +7 -6
- data/docs/stats.md +142 -0
- data/docs/systemd.md +27 -67
- data/ext/puma_http11/PumaHttp11Service.java +2 -4
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +22 -8
- data/ext/puma_http11/http11_parser.c +48 -48
- 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 +4 -2
- data/ext/puma_http11/mini_ssl.c +211 -118
- 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 +5 -7
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
- data/ext/puma_http11/puma_http11.c +32 -50
- data/lib/puma.rb +46 -0
- data/lib/puma/app/status.rb +48 -35
- data/lib/puma/binder.rb +177 -103
- data/lib/puma/cli.rb +11 -15
- data/lib/puma/client.rb +83 -76
- data/lib/puma/cluster.rb +184 -198
- data/lib/puma/cluster/worker.rb +183 -0
- data/lib/puma/cluster/worker_handle.rb +90 -0
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +55 -49
- data/lib/puma/const.rb +13 -5
- data/lib/puma/control_cli.rb +93 -76
- data/lib/puma/detect.rb +24 -3
- data/lib/puma/dsl.rb +266 -92
- 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.rb +96 -0
- data/lib/puma/launcher.rb +113 -45
- data/lib/puma/minissl.rb +114 -33
- data/lib/puma/minissl/context_builder.rb +6 -3
- data/lib/puma/null_io.rb +13 -1
- data/lib/puma/plugin.rb +1 -10
- data/lib/puma/queue_close.rb +26 -0
- data/lib/puma/rack/builder.rb +0 -4
- data/lib/puma/reactor.rb +85 -369
- data/lib/puma/request.rb +467 -0
- data/lib/puma/runner.rb +29 -58
- data/lib/puma/server.rb +267 -698
- data/lib/puma/single.rb +9 -65
- data/lib/puma/state_file.rb +8 -3
- data/lib/puma/systemd.rb +46 -0
- data/lib/puma/thread_pool.rb +119 -53
- data/lib/puma/util.rb +12 -0
- data/lib/rack/handler/puma.rb +2 -3
- data/tools/{docker/Dockerfile → Dockerfile} +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/cli.rb
CHANGED
@@ -80,7 +80,7 @@ module Puma
|
|
80
80
|
@launcher.run
|
81
81
|
end
|
82
82
|
|
83
|
-
|
83
|
+
private
|
84
84
|
def unsupported(str)
|
85
85
|
@events.error(str)
|
86
86
|
raise UnsupportedOption
|
@@ -104,6 +104,10 @@ module Puma
|
|
104
104
|
user_config.bind arg
|
105
105
|
end
|
106
106
|
|
107
|
+
o.on "--bind-to-activated-sockets [only]", "Bind to all activated sockets" do |arg|
|
108
|
+
user_config.bind_to_activated_sockets(arg || true)
|
109
|
+
end
|
110
|
+
|
107
111
|
o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
|
108
112
|
file_config.load arg
|
109
113
|
end
|
@@ -112,21 +116,11 @@ module Puma
|
|
112
116
|
configure_control_url(arg)
|
113
117
|
end
|
114
118
|
|
115
|
-
# alias --control-url for backwards-compatibility
|
116
|
-
o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
|
117
|
-
configure_control_url(arg)
|
118
|
-
end
|
119
|
-
|
120
119
|
o.on "--control-token TOKEN",
|
121
120
|
"The token to use as authentication for the control server" do |arg|
|
122
121
|
@control_options[:auth_token] = arg
|
123
122
|
end
|
124
123
|
|
125
|
-
o.on "-d", "--daemon", "Daemonize the server into the background" do
|
126
|
-
user_config.daemonize
|
127
|
-
user_config.quiet
|
128
|
-
end
|
129
|
-
|
130
124
|
o.on "--debug", "Log lowlevel debugging information" do
|
131
125
|
user_config.debug
|
132
126
|
end
|
@@ -140,6 +134,12 @@ module Puma
|
|
140
134
|
user_config.environment arg
|
141
135
|
end
|
142
136
|
|
137
|
+
o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
|
138
|
+
"Fork new workers from existing worker. Cluster mode only",
|
139
|
+
"Auto-refork after REQUESTS (default 1000)" do |*args|
|
140
|
+
user_config.fork_worker(*args.compact)
|
141
|
+
end
|
142
|
+
|
143
143
|
o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
|
144
144
|
$LOAD_PATH.unshift(*arg.split(':'))
|
145
145
|
end
|
@@ -192,10 +192,6 @@ module Puma
|
|
192
192
|
end
|
193
193
|
end
|
194
194
|
|
195
|
-
o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
|
196
|
-
user_config.tcp_mode!
|
197
|
-
end
|
198
|
-
|
199
195
|
o.on "--early-hints", "Enable early hints support" do
|
200
196
|
user_config.early_hints
|
201
197
|
end
|
data/lib/puma/client.rb
CHANGED
@@ -69,6 +69,7 @@ module Puma
|
|
69
69
|
@hijacked = false
|
70
70
|
|
71
71
|
@peerip = nil
|
72
|
+
@listener = nil
|
72
73
|
@remote_addr_header = nil
|
73
74
|
|
74
75
|
@body_remain = 0
|
@@ -81,10 +82,17 @@ module Puma
|
|
81
82
|
|
82
83
|
attr_writer :peerip
|
83
84
|
|
84
|
-
attr_accessor :remote_addr_header
|
85
|
+
attr_accessor :remote_addr_header, :listener
|
85
86
|
|
86
87
|
def_delegators :@io, :closed?
|
87
88
|
|
89
|
+
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
90
|
+
# used for MiniSSL::Socket
|
91
|
+
def io_ok?
|
92
|
+
@to_io.is_a?(::BasicSocket) && !closed?
|
93
|
+
end
|
94
|
+
|
95
|
+
# @!attribute [r] inspect
|
88
96
|
def inspect
|
89
97
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
90
98
|
end
|
@@ -96,12 +104,18 @@ module Puma
|
|
96
104
|
env[HIJACK_IO] ||= @io
|
97
105
|
end
|
98
106
|
|
107
|
+
# @!attribute [r] in_data_phase
|
99
108
|
def in_data_phase
|
100
109
|
!@read_header
|
101
110
|
end
|
102
111
|
|
103
112
|
def set_timeout(val)
|
104
|
-
@timeout_at =
|
113
|
+
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
|
114
|
+
end
|
115
|
+
|
116
|
+
# Number of seconds until the timeout elapses.
|
117
|
+
def timeout
|
118
|
+
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
105
119
|
end
|
106
120
|
|
107
121
|
def reset(fast_check=true)
|
@@ -113,7 +127,7 @@ module Puma
|
|
113
127
|
@parsed_bytes = 0
|
114
128
|
@ready = false
|
115
129
|
@body_remain = 0
|
116
|
-
@peerip = nil
|
130
|
+
@peerip = nil if @remote_addr_header
|
117
131
|
@in_last_chunk = false
|
118
132
|
|
119
133
|
if @buffer
|
@@ -153,9 +167,11 @@ module Puma
|
|
153
167
|
|
154
168
|
begin
|
155
169
|
data = @io.read_nonblock(CHUNK_SIZE)
|
156
|
-
rescue
|
170
|
+
rescue IO::WaitReadable
|
157
171
|
return false
|
158
|
-
rescue
|
172
|
+
rescue EOFError
|
173
|
+
# Swallow error, don't log
|
174
|
+
rescue SystemCallError, IOError
|
159
175
|
raise ConnectionError, "Connection error detected during read"
|
160
176
|
end
|
161
177
|
|
@@ -184,68 +200,20 @@ module Puma
|
|
184
200
|
false
|
185
201
|
end
|
186
202
|
|
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
|
203
|
+
def eagerly_finish
|
204
|
+
return true if @ready
|
205
|
+
return false unless IO.select([@to_io], nil, nil, 0)
|
206
|
+
try_to_finish
|
207
|
+
end
|
235
208
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
end
|
241
|
-
end # IS_JRUBY
|
209
|
+
def finish(timeout)
|
210
|
+
return if @ready
|
211
|
+
IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
|
212
|
+
end
|
242
213
|
|
243
|
-
def
|
244
|
-
|
245
|
-
|
246
|
-
IO.select([@to_io], nil, nil)
|
247
|
-
end
|
248
|
-
true
|
214
|
+
def timeout!
|
215
|
+
write_error(408) if in_data_phase
|
216
|
+
raise ConnectionError
|
249
217
|
end
|
250
218
|
|
251
219
|
def write_error(status_code)
|
@@ -259,7 +227,7 @@ module Puma
|
|
259
227
|
return @peerip if @peerip
|
260
228
|
|
261
229
|
if @remote_addr_header
|
262
|
-
hdr = (@env[@remote_addr_header] ||
|
230
|
+
hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
|
263
231
|
@peerip = hdr
|
264
232
|
return hdr
|
265
233
|
end
|
@@ -267,6 +235,15 @@ module Puma
|
|
267
235
|
@peerip ||= @io.peeraddr.last
|
268
236
|
end
|
269
237
|
|
238
|
+
# Returns true if the persistent connection can be closed immediately
|
239
|
+
# without waiting for the configured idle/shutdown timeout.
|
240
|
+
# @version 5.0.0
|
241
|
+
#
|
242
|
+
def can_close?
|
243
|
+
# Allow connection to close if we're not in the middle of parsing a request.
|
244
|
+
@parsed_bytes == 0
|
245
|
+
end
|
246
|
+
|
270
247
|
private
|
271
248
|
|
272
249
|
def setup_body
|
@@ -285,8 +262,16 @@ module Puma
|
|
285
262
|
|
286
263
|
te = @env[TRANSFER_ENCODING2]
|
287
264
|
|
288
|
-
if te
|
289
|
-
|
265
|
+
if te
|
266
|
+
if te.include?(",")
|
267
|
+
te.split(",").each do |part|
|
268
|
+
if CHUNKED.casecmp(part.strip) == 0
|
269
|
+
return setup_chunked_body(body)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
elsif CHUNKED.casecmp(te) == 0
|
273
|
+
return setup_chunked_body(body)
|
274
|
+
end
|
290
275
|
end
|
291
276
|
|
292
277
|
@chunked_body = false
|
@@ -311,6 +296,7 @@ module Puma
|
|
311
296
|
|
312
297
|
if remain > MAX_BODY
|
313
298
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
299
|
+
@body.unlink
|
314
300
|
@body.binmode
|
315
301
|
@tempfile = @body
|
316
302
|
else
|
@@ -343,7 +329,7 @@ module Puma
|
|
343
329
|
|
344
330
|
begin
|
345
331
|
chunk = @io.read_nonblock(want)
|
346
|
-
rescue
|
332
|
+
rescue IO::WaitReadable
|
347
333
|
return false
|
348
334
|
rescue SystemCallError, IOError
|
349
335
|
raise ConnectionError, "Connection error detected during read"
|
@@ -389,7 +375,10 @@ module Puma
|
|
389
375
|
raise EOFError
|
390
376
|
end
|
391
377
|
|
392
|
-
|
378
|
+
if decode_chunk(chunk)
|
379
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
380
|
+
return true
|
381
|
+
end
|
393
382
|
end
|
394
383
|
end
|
395
384
|
|
@@ -399,22 +388,40 @@ module Puma
|
|
399
388
|
@prev_chunk = ""
|
400
389
|
|
401
390
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
391
|
+
@body.unlink
|
402
392
|
@body.binmode
|
403
393
|
@tempfile = @body
|
394
|
+
@chunked_content_length = 0
|
404
395
|
|
405
|
-
|
396
|
+
if decode_chunk(body)
|
397
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
398
|
+
return true
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# @version 5.0.0
|
403
|
+
def write_chunk(str)
|
404
|
+
@chunked_content_length += @body.write(str)
|
406
405
|
end
|
407
406
|
|
408
407
|
def decode_chunk(chunk)
|
409
408
|
if @partial_part_left > 0
|
410
409
|
if @partial_part_left <= chunk.size
|
411
410
|
if @partial_part_left > 2
|
412
|
-
|
411
|
+
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
|
413
412
|
end
|
414
413
|
chunk = chunk[@partial_part_left..-1]
|
415
414
|
@partial_part_left = 0
|
416
415
|
else
|
417
|
-
|
416
|
+
if @partial_part_left > 2
|
417
|
+
if @partial_part_left == chunk.size + 1
|
418
|
+
# Don't include the last \r
|
419
|
+
write_chunk(chunk[0..(@partial_part_left-3)])
|
420
|
+
else
|
421
|
+
# don't include the last \r\n
|
422
|
+
write_chunk(chunk)
|
423
|
+
end
|
424
|
+
end
|
418
425
|
@partial_part_left -= chunk.size
|
419
426
|
return false
|
420
427
|
end
|
@@ -461,12 +468,12 @@ module Puma
|
|
461
468
|
|
462
469
|
case
|
463
470
|
when got == len
|
464
|
-
|
471
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
465
472
|
when got <= len - 2
|
466
|
-
|
473
|
+
write_chunk(part)
|
467
474
|
@partial_part_left = len - part.size
|
468
475
|
when got == len - 1 # edge where we get just \r but not \n
|
469
|
-
|
476
|
+
write_chunk(part[0..-2])
|
470
477
|
@partial_part_left = len - part.size
|
471
478
|
end
|
472
479
|
else
|
data/lib/puma/cluster.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'puma/runner'
|
4
4
|
require 'puma/util'
|
5
5
|
require 'puma/plugin'
|
6
|
+
require 'puma/cluster/worker_handle'
|
7
|
+
require 'puma/cluster/worker'
|
6
8
|
|
7
9
|
require 'time'
|
8
10
|
|
@@ -11,10 +13,6 @@ module Puma
|
|
11
13
|
# to boot and serve a Ruby application when puma "workers" are needed
|
12
14
|
# i.e. when using multi-processes. For example `$ puma -w 5`
|
13
15
|
#
|
14
|
-
# At the core of this class is running an instance of `Puma::Server` which
|
15
|
-
# gets created via the `start_server` method from the `Puma::Runner` class
|
16
|
-
# that this inherits from.
|
17
|
-
#
|
18
16
|
# An instance of this class will spawn the number of processes passed in
|
19
17
|
# via the `spawn_workers` method call. Each worker will have it's own
|
20
18
|
# instance of a `Puma::Server`.
|
@@ -24,9 +22,8 @@ module Puma
|
|
24
22
|
|
25
23
|
@phase = 0
|
26
24
|
@workers = []
|
27
|
-
@next_check =
|
25
|
+
@next_check = Time.now
|
28
26
|
|
29
|
-
@phased_state = :idle
|
30
27
|
@phased_restart = false
|
31
28
|
end
|
32
29
|
|
@@ -37,7 +34,7 @@ module Puma
|
|
37
34
|
begin
|
38
35
|
loop do
|
39
36
|
wait_workers
|
40
|
-
break if @workers.empty?
|
37
|
+
break if @workers.reject {|w| w.pid.nil?}.empty?
|
41
38
|
sleep 0.2
|
42
39
|
end
|
43
40
|
rescue Interrupt
|
@@ -46,6 +43,7 @@ module Puma
|
|
46
43
|
end
|
47
44
|
|
48
45
|
def start_phased_restart
|
46
|
+
@events.fire_on_restart!
|
49
47
|
@phase += 1
|
50
48
|
log "- Starting phased worker restart, phase: #{@phase}"
|
51
49
|
|
@@ -62,95 +60,49 @@ module Puma
|
|
62
60
|
@workers.each { |x| x.hup }
|
63
61
|
end
|
64
62
|
|
65
|
-
class Worker
|
66
|
-
def initialize(idx, pid, phase, options)
|
67
|
-
@index = idx
|
68
|
-
@pid = pid
|
69
|
-
@phase = phase
|
70
|
-
@stage = :started
|
71
|
-
@signal = "TERM"
|
72
|
-
@options = options
|
73
|
-
@first_term_sent = nil
|
74
|
-
@started_at = Time.now
|
75
|
-
@last_checkin = Time.now
|
76
|
-
@last_status = '{}'
|
77
|
-
@term = false
|
78
|
-
end
|
79
|
-
|
80
|
-
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
81
|
-
|
82
|
-
def booted?
|
83
|
-
@stage == :booted
|
84
|
-
end
|
85
|
-
|
86
|
-
def boot!
|
87
|
-
@last_checkin = Time.now
|
88
|
-
@stage = :booted
|
89
|
-
end
|
90
|
-
|
91
|
-
def term?
|
92
|
-
@term
|
93
|
-
end
|
94
|
-
|
95
|
-
def ping!(status)
|
96
|
-
@last_checkin = Time.now
|
97
|
-
@last_status = status
|
98
|
-
end
|
99
|
-
|
100
|
-
def ping_timeout?(which)
|
101
|
-
Time.now - @last_checkin > which
|
102
|
-
end
|
103
|
-
|
104
|
-
def term
|
105
|
-
begin
|
106
|
-
if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
|
107
|
-
@signal = "KILL"
|
108
|
-
else
|
109
|
-
@term ||= true
|
110
|
-
@first_term_sent ||= Time.now
|
111
|
-
end
|
112
|
-
Process.kill @signal, @pid
|
113
|
-
rescue Errno::ESRCH
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def kill
|
118
|
-
Process.kill "KILL", @pid
|
119
|
-
rescue Errno::ESRCH
|
120
|
-
end
|
121
|
-
|
122
|
-
def hup
|
123
|
-
Process.kill "HUP", @pid
|
124
|
-
rescue Errno::ESRCH
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
63
|
def spawn_workers
|
129
64
|
diff = @options[:workers] - @workers.size
|
130
65
|
return if diff < 1
|
131
66
|
|
132
67
|
master = Process.pid
|
68
|
+
if @options[:fork_worker]
|
69
|
+
@fork_writer << "-1\n"
|
70
|
+
end
|
133
71
|
|
134
72
|
diff.times do
|
135
73
|
idx = next_worker_index
|
136
|
-
@launcher.config.run_hooks :before_worker_fork, idx
|
137
74
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
75
|
+
if @options[:fork_worker] && idx != 0
|
76
|
+
@fork_writer << "#{idx}\n"
|
77
|
+
pid = nil
|
78
|
+
else
|
79
|
+
pid = spawn_worker(idx, master)
|
143
80
|
end
|
144
81
|
|
145
82
|
debug "Spawned worker: #{pid}"
|
146
|
-
@workers <<
|
83
|
+
@workers << WorkerHandle.new(idx, pid, @phase, @options)
|
84
|
+
end
|
147
85
|
|
148
|
-
|
86
|
+
if @options[:fork_worker] &&
|
87
|
+
@workers.all? {|x| x.phase == @phase}
|
88
|
+
|
89
|
+
@fork_writer << "0\n"
|
149
90
|
end
|
91
|
+
end
|
150
92
|
|
151
|
-
|
152
|
-
|
93
|
+
# @version 5.0.0
|
94
|
+
def spawn_worker(idx, master)
|
95
|
+
@launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
|
96
|
+
|
97
|
+
pid = fork { worker(idx, master) }
|
98
|
+
if !pid
|
99
|
+
log "! Complete inability to spawn new workers detected"
|
100
|
+
log "! Seppuku is the only choice."
|
101
|
+
exit! 1
|
153
102
|
end
|
103
|
+
|
104
|
+
@launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
|
105
|
+
pid
|
154
106
|
end
|
155
107
|
|
156
108
|
def cull_workers
|
@@ -163,11 +115,12 @@ module Puma
|
|
163
115
|
debug "Workers to cull: #{workers_to_cull.inspect}"
|
164
116
|
|
165
117
|
workers_to_cull.each do |worker|
|
166
|
-
log "- Worker #{worker.index} (
|
118
|
+
log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
|
167
119
|
worker.term
|
168
120
|
end
|
169
121
|
end
|
170
122
|
|
123
|
+
# @!attribute [r] next_worker_index
|
171
124
|
def next_worker_index
|
172
125
|
all_positions = 0...@options[:workers]
|
173
126
|
occupied_positions = @workers.map { |w| w.index }
|
@@ -179,26 +132,12 @@ module Puma
|
|
179
132
|
@workers.count { |w| !w.booted? } == 0
|
180
133
|
end
|
181
134
|
|
182
|
-
def check_workers
|
183
|
-
return if
|
135
|
+
def check_workers
|
136
|
+
return if @next_check >= Time.now
|
184
137
|
|
185
138
|
@next_check = Time.now + Const::WORKER_CHECK_INTERVAL
|
186
139
|
|
187
|
-
|
188
|
-
|
189
|
-
@workers.each do |w|
|
190
|
-
next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
|
191
|
-
if w.ping_timeout?(@options[:worker_timeout])
|
192
|
-
log "! Terminating timed out worker: #{w.pid}"
|
193
|
-
w.kill
|
194
|
-
any = true
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# If we killed any timed out workers, try to catch them
|
199
|
-
# during this loop by giving the kernel time to kill them.
|
200
|
-
sleep 1 if any
|
201
|
-
|
140
|
+
timeout_workers
|
202
141
|
wait_workers
|
203
142
|
cull_workers
|
204
143
|
spawn_workers
|
@@ -211,17 +150,18 @@ module Puma
|
|
211
150
|
w = @workers.find { |x| x.phase != @phase }
|
212
151
|
|
213
152
|
if w
|
214
|
-
|
215
|
-
@phased_state = :waiting
|
216
|
-
log "- Stopping #{w.pid} for phased upgrade..."
|
217
|
-
end
|
218
|
-
|
153
|
+
log "- Stopping #{w.pid} for phased upgrade..."
|
219
154
|
unless w.term?
|
220
155
|
w.term
|
221
156
|
log "- #{w.signal} sent to #{w.pid}..."
|
222
157
|
end
|
223
158
|
end
|
224
159
|
end
|
160
|
+
|
161
|
+
@next_check = [
|
162
|
+
@workers.reject(&:term?).map(&:ping_timeout).min,
|
163
|
+
@next_check
|
164
|
+
].compact.min
|
225
165
|
end
|
226
166
|
|
227
167
|
def wakeup!
|
@@ -235,80 +175,25 @@ module Puma
|
|
235
175
|
end
|
236
176
|
|
237
177
|
def worker(index, master)
|
238
|
-
title = "puma: cluster worker #{index}: #{master}"
|
239
|
-
title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
|
240
|
-
$0 = title
|
241
|
-
|
242
|
-
Signal.trap "SIGINT", "IGNORE"
|
243
|
-
|
244
178
|
@workers = []
|
179
|
+
|
245
180
|
@master_read.close
|
246
181
|
@suicide_pipe.close
|
182
|
+
@fork_writer.close
|
247
183
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
exit! 1
|
253
|
-
end
|
254
|
-
|
255
|
-
# If we're not running under a Bundler context, then
|
256
|
-
# report the info about the context we will be using
|
257
|
-
if !ENV['BUNDLE_GEMFILE']
|
258
|
-
if File.exist?("Gemfile")
|
259
|
-
log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
|
260
|
-
elsif File.exist?("gems.rb")
|
261
|
-
log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
|
262
|
-
end
|
184
|
+
pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
|
185
|
+
if @options[:fork_worker]
|
186
|
+
pipes[:fork_pipe] = @fork_pipe
|
187
|
+
pipes[:wakeup] = @wakeup
|
263
188
|
end
|
264
189
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
@worker_write << "e#{Process.pid}\n" rescue nil
|
273
|
-
server.stop
|
274
|
-
end
|
275
|
-
|
276
|
-
begin
|
277
|
-
@worker_write << "b#{Process.pid}\n"
|
278
|
-
rescue SystemCallError, IOError
|
279
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
280
|
-
STDERR.puts "Master seems to have exited, exiting."
|
281
|
-
return
|
282
|
-
end
|
283
|
-
|
284
|
-
Thread.new(@worker_write) do |io|
|
285
|
-
Puma.set_thread_name "stat payload"
|
286
|
-
base_payload = "p#{Process.pid}"
|
287
|
-
|
288
|
-
while true
|
289
|
-
sleep Const::WORKER_CHECK_INTERVAL
|
290
|
-
begin
|
291
|
-
b = server.backlog || 0
|
292
|
-
r = server.running || 0
|
293
|
-
t = server.pool_capacity || 0
|
294
|
-
m = server.max_threads || 0
|
295
|
-
payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
|
296
|
-
io << payload
|
297
|
-
rescue IOError
|
298
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
299
|
-
break
|
300
|
-
end
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
server.run.join
|
305
|
-
|
306
|
-
# Invoke any worker shutdown hooks so they can prevent the worker
|
307
|
-
# exiting until any background operations are completed
|
308
|
-
@launcher.config.run_hooks :before_worker_shutdown, index
|
309
|
-
ensure
|
310
|
-
@worker_write << "t#{Process.pid}\n" rescue nil
|
311
|
-
@worker_write.close
|
190
|
+
server = start_server if preload?
|
191
|
+
new_worker = Worker.new index: index,
|
192
|
+
master: master,
|
193
|
+
launcher: @launcher,
|
194
|
+
pipes: pipes,
|
195
|
+
server: server
|
196
|
+
new_worker.run
|
312
197
|
end
|
313
198
|
|
314
199
|
def restart
|
@@ -350,20 +235,61 @@ module Puma
|
|
350
235
|
|
351
236
|
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
352
237
|
# the master process.
|
238
|
+
# @!attribute [r] stats
|
353
239
|
def stats
|
354
240
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
355
|
-
|
356
|
-
|
357
|
-
|
241
|
+
worker_status = @workers.map do |w|
|
242
|
+
{
|
243
|
+
started_at: w.started_at.utc.iso8601,
|
244
|
+
pid: w.pid,
|
245
|
+
index: w.index,
|
246
|
+
phase: w.phase,
|
247
|
+
booted: w.booted?,
|
248
|
+
last_checkin: w.last_checkin.utc.iso8601,
|
249
|
+
last_status: w.last_status,
|
250
|
+
}
|
251
|
+
end
|
252
|
+
|
253
|
+
{
|
254
|
+
started_at: @started_at.utc.iso8601,
|
255
|
+
workers: @workers.size,
|
256
|
+
phase: @phase,
|
257
|
+
booted_workers: worker_status.count { |w| w[:booted] },
|
258
|
+
old_workers: old_worker_count,
|
259
|
+
worker_status: worker_status,
|
260
|
+
}
|
358
261
|
end
|
359
262
|
|
360
263
|
def preload?
|
361
264
|
@options[:preload_app]
|
362
265
|
end
|
363
266
|
|
267
|
+
# @version 5.0.0
|
268
|
+
def fork_worker!
|
269
|
+
if (worker = @workers.find { |w| w.index == 0 })
|
270
|
+
worker.phase += 1
|
271
|
+
end
|
272
|
+
phased_restart
|
273
|
+
end
|
274
|
+
|
364
275
|
# We do this in a separate method to keep the lambda scope
|
365
276
|
# of the signals handlers as small as possible.
|
366
277
|
def setup_signals
|
278
|
+
if @options[:fork_worker]
|
279
|
+
Signal.trap "SIGURG" do
|
280
|
+
fork_worker!
|
281
|
+
end
|
282
|
+
|
283
|
+
# Auto-fork after the specified number of requests.
|
284
|
+
if (fork_requests = @options[:fork_worker].to_i) > 0
|
285
|
+
@launcher.events.register(:ping!) do |w|
|
286
|
+
fork_worker! if w.index == 0 &&
|
287
|
+
w.phase == 0 &&
|
288
|
+
w.last_status[:requests_count] >= fork_requests
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
367
293
|
Signal.trap "SIGCHLD" do
|
368
294
|
wakeup!
|
369
295
|
end
|
@@ -392,7 +318,7 @@ module Puma
|
|
392
318
|
|
393
319
|
stop_workers
|
394
320
|
stop
|
395
|
-
|
321
|
+
@events.fire_on_stopped!
|
396
322
|
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
397
323
|
exit 0 # Clean exit, workers were stopped
|
398
324
|
end
|
@@ -404,15 +330,25 @@ module Puma
|
|
404
330
|
|
405
331
|
output_header "cluster"
|
406
332
|
|
407
|
-
|
408
|
-
|
409
|
-
before = Thread.list
|
333
|
+
# This is aligned with the output from Runner, see Runner#output_header
|
334
|
+
log "* Workers: #{@options[:workers]}"
|
410
335
|
|
411
336
|
if preload?
|
337
|
+
# Threads explicitly marked as fork safe will be ignored. Used in Rails,
|
338
|
+
# but may be used by anyone. Note that we need to explicit
|
339
|
+
# Process::Waiter check here because there's a bug in Ruby 2.6 and below
|
340
|
+
# where calling thread_variable_get on a Process::Waiter will segfault.
|
341
|
+
# We can drop that clause once those versions of Ruby are no longer
|
342
|
+
# supported.
|
343
|
+
fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
|
344
|
+
|
345
|
+
before = Thread.list.reject(&fork_safe)
|
346
|
+
|
347
|
+
log "* Restarts: (\u2714) hot (\u2716) phased"
|
412
348
|
log "* Preloading application"
|
413
349
|
load_and_bind
|
414
350
|
|
415
|
-
after = Thread.list
|
351
|
+
after = Thread.list.reject(&fork_safe)
|
416
352
|
|
417
353
|
if after.size > before.size
|
418
354
|
threads = (after - before)
|
@@ -426,7 +362,7 @@ module Puma
|
|
426
362
|
end
|
427
363
|
end
|
428
364
|
else
|
429
|
-
log "*
|
365
|
+
log "* Restarts: (\u2714) hot (\u2714) phased"
|
430
366
|
|
431
367
|
unless @launcher.config.app_configured?
|
432
368
|
error "No application configured, nothing to run"
|
@@ -447,12 +383,13 @@ module Puma
|
|
447
383
|
#
|
448
384
|
@check_pipe, @suicide_pipe = Puma::Util.pipe
|
449
385
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
386
|
+
# Separate pipe used by worker 0 to receive commands to
|
387
|
+
# fork new worker processes.
|
388
|
+
@fork_pipe, @fork_writer = Puma::Util.pipe
|
389
|
+
|
390
|
+
log "Use Ctrl-C to stop"
|
391
|
+
|
392
|
+
single_worker_warning
|
456
393
|
|
457
394
|
redirect_io
|
458
395
|
|
@@ -464,7 +401,8 @@ module Puma
|
|
464
401
|
|
465
402
|
@master_read, @worker_write = read, @wakeup
|
466
403
|
|
467
|
-
@launcher.config.run_hooks :before_fork, nil
|
404
|
+
@launcher.config.run_hooks :before_fork, nil, @launcher.events
|
405
|
+
Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
|
468
406
|
|
469
407
|
spawn_workers
|
470
408
|
|
@@ -472,51 +410,67 @@ module Puma
|
|
472
410
|
stop
|
473
411
|
end
|
474
412
|
|
475
|
-
@launcher.events.fire_on_booted!
|
476
|
-
|
477
413
|
begin
|
478
|
-
|
414
|
+
booted = false
|
415
|
+
in_phased_restart = false
|
416
|
+
workers_not_booted = @options[:workers]
|
479
417
|
|
480
418
|
while @status == :run
|
481
419
|
begin
|
482
420
|
if @phased_restart
|
483
421
|
start_phased_restart
|
484
422
|
@phased_restart = false
|
423
|
+
in_phased_restart = true
|
424
|
+
workers_not_booted = @options[:workers]
|
485
425
|
end
|
486
426
|
|
487
|
-
check_workers
|
488
|
-
|
489
|
-
force_check = false
|
427
|
+
check_workers
|
490
428
|
|
491
|
-
res = IO.select([read], nil, nil,
|
429
|
+
res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
|
492
430
|
|
493
431
|
if res
|
494
432
|
req = read.read_nonblock(1)
|
495
433
|
|
434
|
+
@next_check = Time.now if req == "!"
|
496
435
|
next if !req || req == "!"
|
497
436
|
|
498
437
|
result = read.gets
|
499
438
|
pid = result.to_i
|
500
439
|
|
440
|
+
if req == "b" || req == "f"
|
441
|
+
pid, idx = result.split(':').map(&:to_i)
|
442
|
+
w = @workers.find {|x| x.index == idx}
|
443
|
+
w.pid = pid if w.pid.nil?
|
444
|
+
end
|
445
|
+
|
501
446
|
if w = @workers.find { |x| x.pid == pid }
|
502
447
|
case req
|
503
448
|
when "b"
|
504
449
|
w.boot!
|
505
|
-
log "- Worker #{w.index} (
|
506
|
-
|
450
|
+
log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
|
451
|
+
@next_check = Time.now
|
452
|
+
workers_not_booted -= 1
|
507
453
|
when "e"
|
508
454
|
# external term, see worker method, Signal.trap "SIGTERM"
|
509
455
|
w.instance_variable_set :@term, true
|
510
456
|
when "t"
|
511
457
|
w.term unless w.term?
|
512
|
-
force_check = true
|
513
458
|
when "p"
|
514
459
|
w.ping!(result.sub(/^\d+/,'').chomp)
|
460
|
+
@launcher.events.fire(:ping!, w)
|
461
|
+
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
462
|
+
@launcher.events.fire_on_booted!
|
463
|
+
booted = true
|
464
|
+
end
|
515
465
|
end
|
516
466
|
else
|
517
467
|
log "! Out-of-sync worker list, no #{pid} worker"
|
518
468
|
end
|
519
469
|
end
|
470
|
+
if in_phased_restart && workers_not_booted.zero?
|
471
|
+
@events.fire_on_booted!
|
472
|
+
in_phased_restart = false
|
473
|
+
end
|
520
474
|
|
521
475
|
rescue Interrupt
|
522
476
|
@status = :stop
|
@@ -534,10 +488,20 @@ module Puma
|
|
534
488
|
|
535
489
|
private
|
536
490
|
|
491
|
+
def single_worker_warning
|
492
|
+
return if @options[:workers] != 1 || @options[:silence_single_worker_warning]
|
493
|
+
|
494
|
+
log "! WARNING: Detected running cluster mode with 1 worker."
|
495
|
+
log "! Running Puma in cluster mode with a single worker is often a misconfiguration."
|
496
|
+
log "! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead."
|
497
|
+
log "! Set the `silence_single_worker_warning` option to silence this warning message."
|
498
|
+
end
|
499
|
+
|
537
500
|
# loops thru @workers, removing workers that exited, and calling
|
538
501
|
# `#term` if needed
|
539
502
|
def wait_workers
|
540
503
|
@workers.reject! do |w|
|
504
|
+
next false if w.pid.nil?
|
541
505
|
begin
|
542
506
|
if Process.wait(w.pid, Process::WNOHANG)
|
543
507
|
true
|
@@ -546,7 +510,29 @@ module Puma
|
|
546
510
|
nil
|
547
511
|
end
|
548
512
|
rescue Errno::ECHILD
|
549
|
-
|
513
|
+
begin
|
514
|
+
Process.kill(0, w.pid)
|
515
|
+
# child still alive but has another parent (e.g., using fork_worker)
|
516
|
+
w.term if w.term?
|
517
|
+
false
|
518
|
+
rescue Errno::ESRCH, Errno::EPERM
|
519
|
+
true # child is already terminated
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# @version 5.0.0
|
526
|
+
def timeout_workers
|
527
|
+
@workers.each do |w|
|
528
|
+
if !w.term? && w.ping_timeout <= Time.now
|
529
|
+
details = if w.booted?
|
530
|
+
"(worker failed to check in within #{@options[:worker_timeout]} seconds)"
|
531
|
+
else
|
532
|
+
"(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
|
533
|
+
end
|
534
|
+
log "! Terminating timed out worker #{details}: #{w.pid}"
|
535
|
+
w.kill
|
550
536
|
end
|
551
537
|
end
|
552
538
|
end
|