puma 5.0.2 → 5.0.3
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 +608 -566
- data/README.md +4 -4
- data/bin/puma-wild +3 -9
- data/docs/deployment.md +5 -6
- data/docs/jungle/README.md +0 -4
- data/docs/jungle/rc.d/puma +2 -2
- data/docs/nginx.md +1 -1
- data/docs/restart.md +46 -23
- data/docs/systemd.md +1 -1
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/mini_ssl.c +39 -37
- data/ext/puma_http11/puma_http11.c +17 -10
- data/lib/puma/app/status.rb +44 -43
- data/lib/puma/binder.rb +9 -1
- data/lib/puma/client.rb +24 -72
- data/lib/puma/cluster.rb +25 -196
- data/lib/puma/cluster/worker.rb +170 -0
- data/lib/puma/cluster/worker_handle.rb +83 -0
- data/lib/puma/configuration.rb +8 -7
- data/lib/puma/const.rb +1 -1
- data/lib/puma/launcher.rb +5 -9
- data/lib/puma/queue_close.rb +26 -0
- data/lib/puma/reactor.rb +77 -362
- data/lib/puma/request.rb +438 -0
- data/lib/puma/runner.rb +4 -17
- data/lib/puma/server.rb +166 -501
- data/lib/puma/single.rb +2 -2
- data/lib/puma/util.rb +11 -0
- metadata +6 -6
- data/docs/jungle/upstart/README.md +0 -61
- data/docs/jungle/upstart/puma-manager.conf +0 -31
- data/docs/jungle/upstart/puma.conf +0 -69
- data/lib/puma/accept_nonblock.rb +0 -29
data/lib/puma/app/status.rb
CHANGED
@@ -7,11 +7,16 @@ module Puma
|
|
7
7
|
class Status
|
8
8
|
OK_STATUS = '{ "status": "ok" }'.freeze
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
# @param launcher [::Puma::Launcher]
|
11
|
+
# @param token [String, nil] the token used for authentication
|
12
|
+
#
|
13
|
+
def initialize(launcher, token = nil)
|
14
|
+
@launcher = launcher
|
12
15
|
@auth_token = token
|
13
16
|
end
|
14
17
|
|
18
|
+
# most commands call methods in `::Puma::Launcher` based on command in
|
19
|
+
# `env['PATH_INFO']`
|
15
20
|
def call(env)
|
16
21
|
unless authenticate(env)
|
17
22
|
return rack_response(403, 'Invalid auth token', 'text/plain')
|
@@ -21,57 +26,53 @@ module Puma
|
|
21
26
|
require 'json'
|
22
27
|
end
|
23
28
|
|
24
|
-
case
|
25
|
-
|
26
|
-
|
27
|
-
|
29
|
+
# resp_type is processed by following case statement, return
|
30
|
+
# is a number (status) or a string used as the body of a 200 response
|
31
|
+
resp_type =
|
32
|
+
case env['PATH_INFO'][/\/([^\/]+)$/, 1]
|
33
|
+
when 'stop'
|
34
|
+
@launcher.stop ; 200
|
28
35
|
|
29
|
-
|
30
|
-
|
31
|
-
rack_response(200, OK_STATUS)
|
36
|
+
when 'halt'
|
37
|
+
@launcher.halt ; 200
|
32
38
|
|
33
|
-
|
34
|
-
|
35
|
-
rack_response(200, OK_STATUS)
|
39
|
+
when 'restart'
|
40
|
+
@launcher.restart ; 200
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
rack_response(404, '{ "error": "phased restart not available" }')
|
40
|
-
else
|
41
|
-
rack_response(200, OK_STATUS)
|
42
|
-
end
|
43
|
-
|
44
|
-
when /\/reload-worker-directory$/
|
45
|
-
if !@cli.send(:reload_worker_directory)
|
46
|
-
rack_response(404, '{ "error": "reload_worker_directory not available" }')
|
47
|
-
else
|
48
|
-
rack_response(200, OK_STATUS)
|
49
|
-
end
|
42
|
+
when 'phased-restart'
|
43
|
+
@launcher.phased_restart ? 200 : 404
|
50
44
|
|
51
|
-
|
52
|
-
|
53
|
-
rack_response(200, OK_STATUS)
|
45
|
+
when 'reload-worker-directory'
|
46
|
+
@launcher.send(:reload_worker_directory) ? 200 : 404
|
54
47
|
|
55
|
-
|
56
|
-
|
48
|
+
when 'gc'
|
49
|
+
GC.start ; 200
|
57
50
|
|
58
|
-
|
59
|
-
|
51
|
+
when 'gc-stats'
|
52
|
+
GC.stat.to_json
|
60
53
|
|
61
|
-
|
62
|
-
|
63
|
-
@cli.thread_status do |name, backtrace|
|
64
|
-
backtraces << { name: name, backtrace: backtrace }
|
65
|
-
end
|
54
|
+
when 'stats'
|
55
|
+
@launcher.stats.to_json
|
66
56
|
|
67
|
-
|
57
|
+
when 'thread-backtraces'
|
58
|
+
backtraces = []
|
59
|
+
@launcher.thread_status do |name, backtrace|
|
60
|
+
backtraces << { name: name, backtrace: backtrace }
|
61
|
+
end
|
62
|
+
backtraces.to_json
|
68
63
|
|
69
|
-
|
70
|
-
|
71
|
-
|
64
|
+
else
|
65
|
+
return rack_response(404, "Unsupported action", 'text/plain')
|
66
|
+
end
|
72
67
|
|
73
|
-
|
74
|
-
|
68
|
+
case resp_type
|
69
|
+
when String
|
70
|
+
rack_response 200, resp_type
|
71
|
+
when 200
|
72
|
+
rack_response 200, OK_STATUS
|
73
|
+
when 404
|
74
|
+
str = env['PATH_INFO'][/\/(\S+)/, 1].tr '-', '_'
|
75
|
+
rack_response 404, "{ \"error\": \"#{str} not available\" }"
|
75
76
|
end
|
76
77
|
end
|
77
78
|
|
data/lib/puma/binder.rb
CHANGED
@@ -12,7 +12,15 @@ module Puma
|
|
12
12
|
if HAS_SSL
|
13
13
|
require 'puma/minissl'
|
14
14
|
require 'puma/minissl/context_builder'
|
15
|
-
|
15
|
+
|
16
|
+
# Odd bug in 'pure Ruby' nio4r verion 2.5.2, which installs with Ruby 2.3.
|
17
|
+
# NIO doesn't create any OpenSSL objects, but it rescues an OpenSSL error.
|
18
|
+
# The bug was that it did not require openssl.
|
19
|
+
# @todo remove when Ruby 2.3 support is dropped
|
20
|
+
#
|
21
|
+
if windows? && RbConfig::CONFIG['ruby_version'] == '2.3.0'
|
22
|
+
require 'openssl'
|
23
|
+
end
|
16
24
|
end
|
17
25
|
|
18
26
|
class Binder
|
data/lib/puma/client.rb
CHANGED
@@ -85,6 +85,12 @@ module Puma
|
|
85
85
|
|
86
86
|
def_delegators :@io, :closed?
|
87
87
|
|
88
|
+
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
89
|
+
# used for MiniSSL::Socket
|
90
|
+
def io_ok?
|
91
|
+
@to_io.is_a?(::BasicSocket) && !closed?
|
92
|
+
end
|
93
|
+
|
88
94
|
# @!attribute [r] inspect
|
89
95
|
def inspect
|
90
96
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
@@ -103,7 +109,12 @@ module Puma
|
|
103
109
|
end
|
104
110
|
|
105
111
|
def set_timeout(val)
|
106
|
-
@timeout_at =
|
112
|
+
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
|
113
|
+
end
|
114
|
+
|
115
|
+
# Number of seconds until the timeout elapses.
|
116
|
+
def timeout
|
117
|
+
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
107
118
|
end
|
108
119
|
|
109
120
|
def reset(fast_check=true)
|
@@ -188,79 +199,20 @@ module Puma
|
|
188
199
|
false
|
189
200
|
end
|
190
201
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
data = @io.sysread_nonblock(CHUNK_SIZE)
|
197
|
-
rescue OpenSSL::SSL::SSLError => e
|
198
|
-
return false if e.kind_of? IO::WaitReadable
|
199
|
-
raise e
|
200
|
-
end
|
201
|
-
|
202
|
-
# No data means a closed socket
|
203
|
-
unless data
|
204
|
-
@buffer = nil
|
205
|
-
set_ready
|
206
|
-
raise EOFError
|
207
|
-
end
|
208
|
-
|
209
|
-
if @buffer
|
210
|
-
@buffer << data
|
211
|
-
else
|
212
|
-
@buffer = data
|
213
|
-
end
|
214
|
-
|
215
|
-
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
216
|
-
|
217
|
-
if @parser.finished?
|
218
|
-
return setup_body
|
219
|
-
elsif @parsed_bytes >= MAX_HEADER
|
220
|
-
raise HttpParserError,
|
221
|
-
"HEADER is longer than allowed, aborting client early."
|
222
|
-
end
|
223
|
-
|
224
|
-
false
|
225
|
-
end
|
226
|
-
|
227
|
-
def eagerly_finish
|
228
|
-
return true if @ready
|
229
|
-
|
230
|
-
if @io.kind_of? OpenSSL::SSL::SSLSocket
|
231
|
-
return true if jruby_start_try_to_finish
|
232
|
-
end
|
233
|
-
|
234
|
-
return false unless IO.select([@to_io], nil, nil, 0)
|
235
|
-
try_to_finish
|
236
|
-
end
|
237
|
-
|
238
|
-
else
|
239
|
-
|
240
|
-
def eagerly_finish
|
241
|
-
return true if @ready
|
242
|
-
return false unless IO.select([@to_io], nil, nil, 0)
|
243
|
-
try_to_finish
|
244
|
-
end
|
245
|
-
|
246
|
-
# For documentation, see https://github.com/puma/puma/issues/1754
|
247
|
-
send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
|
248
|
-
end # IS_JRUBY
|
202
|
+
def eagerly_finish
|
203
|
+
return true if @ready
|
204
|
+
return false unless IO.select([@to_io], nil, nil, 0)
|
205
|
+
try_to_finish
|
206
|
+
end
|
249
207
|
|
250
208
|
def finish(timeout)
|
251
|
-
return
|
252
|
-
until try_to_finish
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
unless can_read
|
259
|
-
write_error(408) if in_data_phase
|
260
|
-
raise ConnectionError
|
261
|
-
end
|
262
|
-
end
|
263
|
-
true
|
209
|
+
return if @ready
|
210
|
+
IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
|
211
|
+
end
|
212
|
+
|
213
|
+
def timeout!
|
214
|
+
write_error(408) if in_data_phase
|
215
|
+
raise ConnectionError
|
264
216
|
end
|
265
217
|
|
266
218
|
def write_error(status_code)
|
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`.
|
@@ -61,79 +59,6 @@ module Puma
|
|
61
59
|
@workers.each { |x| x.hup }
|
62
60
|
end
|
63
61
|
|
64
|
-
class Worker
|
65
|
-
def initialize(idx, pid, phase, options)
|
66
|
-
@index = idx
|
67
|
-
@pid = pid
|
68
|
-
@phase = phase
|
69
|
-
@stage = :started
|
70
|
-
@signal = "TERM"
|
71
|
-
@options = options
|
72
|
-
@first_term_sent = nil
|
73
|
-
@started_at = Time.now
|
74
|
-
@last_checkin = Time.now
|
75
|
-
@last_status = {}
|
76
|
-
@term = false
|
77
|
-
end
|
78
|
-
|
79
|
-
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
80
|
-
|
81
|
-
# @version 5.0.0
|
82
|
-
attr_writer :pid, :phase
|
83
|
-
|
84
|
-
def booted?
|
85
|
-
@stage == :booted
|
86
|
-
end
|
87
|
-
|
88
|
-
def boot!
|
89
|
-
@last_checkin = Time.now
|
90
|
-
@stage = :booted
|
91
|
-
end
|
92
|
-
|
93
|
-
def term?
|
94
|
-
@term
|
95
|
-
end
|
96
|
-
|
97
|
-
def ping!(status)
|
98
|
-
@last_checkin = Time.now
|
99
|
-
require 'json'
|
100
|
-
@last_status = JSON.parse(status, symbolize_names: true)
|
101
|
-
end
|
102
|
-
|
103
|
-
# @see Puma::Cluster#check_workers
|
104
|
-
# @version 5.0.0
|
105
|
-
def ping_timeout
|
106
|
-
@last_checkin +
|
107
|
-
(booted? ?
|
108
|
-
@options[:worker_timeout] :
|
109
|
-
@options[:worker_boot_timeout]
|
110
|
-
)
|
111
|
-
end
|
112
|
-
|
113
|
-
def term
|
114
|
-
begin
|
115
|
-
if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
|
116
|
-
@signal = "KILL"
|
117
|
-
else
|
118
|
-
@term ||= true
|
119
|
-
@first_term_sent ||= Time.now
|
120
|
-
end
|
121
|
-
Process.kill @signal, @pid if @pid
|
122
|
-
rescue Errno::ESRCH
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def kill
|
127
|
-
@signal = 'KILL'
|
128
|
-
term
|
129
|
-
end
|
130
|
-
|
131
|
-
def hup
|
132
|
-
Process.kill "HUP", @pid
|
133
|
-
rescue Errno::ESRCH
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
62
|
def spawn_workers
|
138
63
|
diff = @options[:workers] - @workers.size
|
139
64
|
return if diff < 1
|
@@ -154,7 +79,7 @@ module Puma
|
|
154
79
|
end
|
155
80
|
|
156
81
|
debug "Spawned worker: #{pid}"
|
157
|
-
@workers <<
|
82
|
+
@workers << WorkerHandle.new(idx, pid, @phase, @options)
|
158
83
|
end
|
159
84
|
|
160
85
|
if @options[:fork_worker] &&
|
@@ -249,113 +174,23 @@ module Puma
|
|
249
174
|
end
|
250
175
|
|
251
176
|
def worker(index, master)
|
252
|
-
title = "puma: cluster worker #{index}: #{master}"
|
253
|
-
title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
|
254
|
-
$0 = title
|
255
|
-
|
256
|
-
Signal.trap "SIGINT", "IGNORE"
|
257
|
-
Signal.trap "SIGCHLD", "DEFAULT"
|
258
|
-
|
259
|
-
fork_worker = @options[:fork_worker] && index == 0
|
260
|
-
|
261
177
|
@workers = []
|
262
|
-
if !@options[:fork_worker] || fork_worker
|
263
|
-
@master_read.close
|
264
|
-
@suicide_pipe.close
|
265
|
-
@fork_writer.close
|
266
|
-
end
|
267
|
-
|
268
|
-
Thread.new do
|
269
|
-
Puma.set_thread_name "worker check pipe"
|
270
|
-
IO.select [@check_pipe]
|
271
|
-
log "! Detected parent died, dying"
|
272
|
-
exit! 1
|
273
|
-
end
|
274
178
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
if File.exist?("Gemfile")
|
279
|
-
log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
|
280
|
-
elsif File.exist?("gems.rb")
|
281
|
-
log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
# Invoke any worker boot hooks so they can get
|
286
|
-
# things in shape before booting the app.
|
287
|
-
@launcher.config.run_hooks :before_worker_boot, index, @launcher.events
|
288
|
-
|
289
|
-
server = @server ||= start_server
|
290
|
-
restart_server = Queue.new << true << false
|
291
|
-
|
292
|
-
if fork_worker
|
293
|
-
restart_server.clear
|
294
|
-
worker_pids = []
|
295
|
-
Signal.trap "SIGCHLD" do
|
296
|
-
wakeup! if worker_pids.reject! do |p|
|
297
|
-
Process.wait(p, Process::WNOHANG) rescue true
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
Thread.new do
|
302
|
-
Puma.set_thread_name "worker fork pipe"
|
303
|
-
while (idx = @fork_pipe.gets)
|
304
|
-
idx = idx.to_i
|
305
|
-
if idx == -1 # stop server
|
306
|
-
if restart_server.length > 0
|
307
|
-
restart_server.clear
|
308
|
-
server.begin_restart(true)
|
309
|
-
@launcher.config.run_hooks :before_refork, nil, @launcher.events
|
310
|
-
nakayoshi_gc
|
311
|
-
end
|
312
|
-
elsif idx == 0 # restart server
|
313
|
-
restart_server << true << false
|
314
|
-
else # fork worker
|
315
|
-
worker_pids << pid = spawn_worker(idx, master)
|
316
|
-
@worker_write << "f#{pid}:#{idx}\n" rescue nil
|
317
|
-
end
|
318
|
-
end
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
Signal.trap "SIGTERM" do
|
323
|
-
@worker_write << "e#{Process.pid}\n" rescue nil
|
324
|
-
server.stop
|
325
|
-
restart_server << false
|
326
|
-
end
|
327
|
-
|
328
|
-
begin
|
329
|
-
@worker_write << "b#{Process.pid}:#{index}\n"
|
330
|
-
rescue SystemCallError, IOError
|
331
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
332
|
-
STDERR.puts "Master seems to have exited, exiting."
|
333
|
-
return
|
334
|
-
end
|
335
|
-
|
336
|
-
Thread.new(@worker_write) do |io|
|
337
|
-
Puma.set_thread_name "stat payload"
|
179
|
+
@master_read.close
|
180
|
+
@suicide_pipe.close
|
181
|
+
@fork_writer.close
|
338
182
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
io << "p#{Process.pid}#{server.stats.to_json}\n"
|
344
|
-
rescue IOError
|
345
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
346
|
-
break
|
347
|
-
end
|
348
|
-
end
|
183
|
+
pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
|
184
|
+
if @options[:fork_worker]
|
185
|
+
pipes[:fork_pipe] = @fork_pipe
|
186
|
+
pipes[:wakeup] = @wakeup
|
349
187
|
end
|
350
188
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
ensure
|
357
|
-
@worker_write << "t#{Process.pid}\n" rescue nil
|
358
|
-
@worker_write.close
|
189
|
+
new_worker = Worker.new index: index,
|
190
|
+
master: master,
|
191
|
+
launcher: @launcher,
|
192
|
+
pipes: pipes
|
193
|
+
new_worker.run
|
359
194
|
end
|
360
195
|
|
361
196
|
def restart
|
@@ -552,7 +387,7 @@ module Puma
|
|
552
387
|
@master_read, @worker_write = read, @wakeup
|
553
388
|
|
554
389
|
@launcher.config.run_hooks :before_fork, nil, @launcher.events
|
555
|
-
nakayoshi_gc
|
390
|
+
Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
|
556
391
|
|
557
392
|
spawn_workers
|
558
393
|
|
@@ -560,9 +395,9 @@ module Puma
|
|
560
395
|
stop
|
561
396
|
end
|
562
397
|
|
563
|
-
@launcher.events.fire_on_booted!
|
564
|
-
|
565
398
|
begin
|
399
|
+
booted = false
|
400
|
+
|
566
401
|
while @status == :run
|
567
402
|
begin
|
568
403
|
if @phased_restart
|
@@ -603,6 +438,10 @@ module Puma
|
|
603
438
|
when "p"
|
604
439
|
w.ping!(result.sub(/^\d+/,'').chomp)
|
605
440
|
@launcher.events.fire(:ping!, w)
|
441
|
+
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
442
|
+
@launcher.events.fire_on_booted!
|
443
|
+
booted = true
|
444
|
+
end
|
606
445
|
end
|
607
446
|
else
|
608
447
|
log "! Out-of-sync worker list, no #{pid} worker"
|
@@ -640,7 +479,9 @@ module Puma
|
|
640
479
|
rescue Errno::ECHILD
|
641
480
|
begin
|
642
481
|
Process.kill(0, w.pid)
|
643
|
-
|
482
|
+
# child still alive but has another parent (e.g., using fork_worker)
|
483
|
+
w.term if w.term?
|
484
|
+
false
|
644
485
|
rescue Errno::ESRCH, Errno::EPERM
|
645
486
|
true # child is already terminated
|
646
487
|
end
|
@@ -657,17 +498,5 @@ module Puma
|
|
657
498
|
end
|
658
499
|
end
|
659
500
|
end
|
660
|
-
|
661
|
-
# @version 5.0.0
|
662
|
-
def nakayoshi_gc
|
663
|
-
return unless @options[:nakayoshi_fork]
|
664
|
-
log "! Promoting existing objects to old generation..."
|
665
|
-
4.times { GC.start(full_mark: false) }
|
666
|
-
if GC.respond_to?(:compact)
|
667
|
-
log "! Compacting..."
|
668
|
-
GC.compact
|
669
|
-
end
|
670
|
-
log "! Friendly fork preparation complete."
|
671
|
-
end
|
672
501
|
end
|
673
502
|
end
|