puma 4.1.1 → 5.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 +149 -10
- data/LICENSE +23 -20
- data/README.md +30 -46
- data/docs/architecture.md +3 -3
- data/docs/deployment.md +9 -3
- data/docs/fork_worker.md +31 -0
- data/docs/jungle/README.md +13 -0
- data/{tools → docs}/jungle/rc.d/README.md +0 -0
- data/{tools → docs}/jungle/rc.d/puma +0 -0
- data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
- data/{tools → docs}/jungle/upstart/README.md +0 -0
- data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
- data/{tools → docs}/jungle/upstart/puma.conf +0 -0
- data/docs/plugins.md +20 -10
- data/docs/signals.md +7 -6
- data/docs/systemd.md +1 -63
- data/ext/puma_http11/PumaHttp11Service.java +2 -4
- data/ext/puma_http11/extconf.rb +6 -0
- data/ext/puma_http11/http11_parser.c +40 -63
- data/ext/puma_http11/http11_parser.java.rl +21 -37
- data/ext/puma_http11/http11_parser.rl +3 -1
- data/ext/puma_http11/http11_parser_common.rl +3 -3
- data/ext/puma_http11/mini_ssl.c +15 -2
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
- data/ext/puma_http11/puma_http11.c +9 -38
- data/lib/puma.rb +23 -0
- data/lib/puma/app/status.rb +46 -30
- data/lib/puma/binder.rb +112 -124
- data/lib/puma/cli.rb +11 -15
- data/lib/puma/client.rb +250 -209
- data/lib/puma/cluster.rb +203 -85
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +31 -42
- data/lib/puma/const.rb +24 -19
- data/lib/puma/control_cli.rb +46 -17
- data/lib/puma/detect.rb +17 -0
- data/lib/puma/dsl.rb +162 -70
- data/lib/puma/error_logger.rb +97 -0
- data/lib/puma/events.rb +35 -31
- data/lib/puma/io_buffer.rb +9 -2
- data/lib/puma/jruby_restart.rb +0 -58
- data/lib/puma/launcher.rb +117 -58
- data/lib/puma/minissl.rb +60 -18
- data/lib/puma/minissl/context_builder.rb +73 -0
- data/lib/puma/null_io.rb +1 -1
- data/lib/puma/plugin.rb +6 -12
- data/lib/puma/rack/builder.rb +0 -4
- data/lib/puma/reactor.rb +16 -9
- data/lib/puma/runner.rb +11 -32
- data/lib/puma/server.rb +173 -193
- data/lib/puma/single.rb +7 -64
- data/lib/puma/state_file.rb +6 -3
- data/lib/puma/thread_pool.rb +104 -81
- data/lib/rack/handler/puma.rb +1 -5
- data/tools/Dockerfile +16 -0
- data/tools/trickletest.rb +0 -1
- metadata +23 -24
- data/ext/puma_http11/io_buffer.c +0 -155
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
- data/lib/puma/convenient.rb +0 -25
- data/lib/puma/daemon_ext.rb +0 -33
- data/lib/puma/delegation.rb +0 -13
- 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/lib/puma/cluster.rb
CHANGED
@@ -24,9 +24,8 @@ module Puma
|
|
24
24
|
|
25
25
|
@phase = 0
|
26
26
|
@workers = []
|
27
|
-
@next_check =
|
27
|
+
@next_check = Time.now
|
28
28
|
|
29
|
-
@phased_state = :idle
|
30
29
|
@phased_restart = false
|
31
30
|
end
|
32
31
|
|
@@ -37,7 +36,7 @@ module Puma
|
|
37
36
|
begin
|
38
37
|
loop do
|
39
38
|
wait_workers
|
40
|
-
break if @workers.empty?
|
39
|
+
break if @workers.reject {|w| w.pid.nil?}.empty?
|
41
40
|
sleep 0.2
|
42
41
|
end
|
43
42
|
rescue Interrupt
|
@@ -73,13 +72,15 @@ module Puma
|
|
73
72
|
@first_term_sent = nil
|
74
73
|
@started_at = Time.now
|
75
74
|
@last_checkin = Time.now
|
76
|
-
@last_status =
|
77
|
-
@dead = false
|
75
|
+
@last_status = {}
|
78
76
|
@term = false
|
79
77
|
end
|
80
78
|
|
81
79
|
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
82
80
|
|
81
|
+
# @version 5.0.0
|
82
|
+
attr_writer :pid, :phase
|
83
|
+
|
83
84
|
def booted?
|
84
85
|
@stage == :booted
|
85
86
|
end
|
@@ -89,25 +90,24 @@ module Puma
|
|
89
90
|
@stage = :booted
|
90
91
|
end
|
91
92
|
|
92
|
-
def dead?
|
93
|
-
@dead
|
94
|
-
end
|
95
|
-
|
96
|
-
def dead!
|
97
|
-
@dead = true
|
98
|
-
end
|
99
|
-
|
100
93
|
def term?
|
101
94
|
@term
|
102
95
|
end
|
103
96
|
|
104
97
|
def ping!(status)
|
105
98
|
@last_checkin = Time.now
|
106
|
-
|
99
|
+
require 'json'
|
100
|
+
@last_status = JSON.parse(status, symbolize_names: true)
|
107
101
|
end
|
108
102
|
|
109
|
-
|
110
|
-
|
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
111
|
end
|
112
112
|
|
113
113
|
def term
|
@@ -118,14 +118,14 @@ module Puma
|
|
118
118
|
@term ||= true
|
119
119
|
@first_term_sent ||= Time.now
|
120
120
|
end
|
121
|
-
Process.kill @signal, @pid
|
121
|
+
Process.kill @signal, @pid if @pid
|
122
122
|
rescue Errno::ESRCH
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
126
126
|
def kill
|
127
|
-
|
128
|
-
|
127
|
+
@signal = 'KILL'
|
128
|
+
term
|
129
129
|
end
|
130
130
|
|
131
131
|
def hup
|
@@ -139,27 +139,44 @@ module Puma
|
|
139
139
|
return if diff < 1
|
140
140
|
|
141
141
|
master = Process.pid
|
142
|
+
if @options[:fork_worker]
|
143
|
+
@fork_writer << "-1\n"
|
144
|
+
end
|
142
145
|
|
143
146
|
diff.times do
|
144
147
|
idx = next_worker_index
|
145
|
-
@launcher.config.run_hooks :before_worker_fork, idx
|
146
148
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
149
|
+
if @options[:fork_worker] && idx != 0
|
150
|
+
@fork_writer << "#{idx}\n"
|
151
|
+
pid = nil
|
152
|
+
else
|
153
|
+
pid = spawn_worker(idx, master)
|
152
154
|
end
|
153
155
|
|
154
156
|
debug "Spawned worker: #{pid}"
|
155
157
|
@workers << Worker.new(idx, pid, @phase, @options)
|
158
|
+
end
|
159
|
+
|
160
|
+
if @options[:fork_worker] &&
|
161
|
+
@workers.all? {|x| x.phase == @phase}
|
156
162
|
|
157
|
-
@
|
163
|
+
@fork_writer << "0\n"
|
158
164
|
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# @version 5.0.0
|
168
|
+
def spawn_worker(idx, master)
|
169
|
+
@launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
|
159
170
|
|
160
|
-
|
161
|
-
|
171
|
+
pid = fork { worker(idx, master) }
|
172
|
+
if !pid
|
173
|
+
log "! Complete inability to spawn new workers detected"
|
174
|
+
log "! Seppuku is the only choice."
|
175
|
+
exit! 1
|
162
176
|
end
|
177
|
+
|
178
|
+
@launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
|
179
|
+
pid
|
163
180
|
end
|
164
181
|
|
165
182
|
def cull_workers
|
@@ -188,26 +205,12 @@ module Puma
|
|
188
205
|
@workers.count { |w| !w.booted? } == 0
|
189
206
|
end
|
190
207
|
|
191
|
-
def check_workers
|
192
|
-
return if
|
208
|
+
def check_workers
|
209
|
+
return if @next_check >= Time.now
|
193
210
|
|
194
211
|
@next_check = Time.now + Const::WORKER_CHECK_INTERVAL
|
195
212
|
|
196
|
-
|
197
|
-
|
198
|
-
@workers.each do |w|
|
199
|
-
next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
|
200
|
-
if w.ping_timeout?(@options[:worker_timeout])
|
201
|
-
log "! Terminating timed out worker: #{w.pid}"
|
202
|
-
w.kill
|
203
|
-
any = true
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
# If we killed any timed out workers, try to catch them
|
208
|
-
# during this loop by giving the kernel time to kill them.
|
209
|
-
sleep 1 if any
|
210
|
-
|
213
|
+
timeout_workers
|
211
214
|
wait_workers
|
212
215
|
cull_workers
|
213
216
|
spawn_workers
|
@@ -220,15 +223,18 @@ module Puma
|
|
220
223
|
w = @workers.find { |x| x.phase != @phase }
|
221
224
|
|
222
225
|
if w
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
+
log "- Stopping #{w.pid} for phased upgrade..."
|
227
|
+
unless w.term?
|
228
|
+
w.term
|
229
|
+
log "- #{w.signal} sent to #{w.pid}..."
|
226
230
|
end
|
227
|
-
|
228
|
-
w.term
|
229
|
-
log "- #{w.signal} sent to #{w.pid}..."
|
230
231
|
end
|
231
232
|
end
|
233
|
+
|
234
|
+
@next_check = [
|
235
|
+
@workers.reject(&:term?).map(&:ping_timeout).min,
|
236
|
+
@next_check
|
237
|
+
].compact.min
|
232
238
|
end
|
233
239
|
|
234
240
|
def wakeup!
|
@@ -247,12 +253,19 @@ module Puma
|
|
247
253
|
$0 = title
|
248
254
|
|
249
255
|
Signal.trap "SIGINT", "IGNORE"
|
256
|
+
Signal.trap "SIGCHLD", "DEFAULT"
|
257
|
+
|
258
|
+
fork_worker = @options[:fork_worker] && index == 0
|
250
259
|
|
251
260
|
@workers = []
|
252
|
-
|
253
|
-
|
261
|
+
if !@options[:fork_worker] || fork_worker
|
262
|
+
@master_read.close
|
263
|
+
@suicide_pipe.close
|
264
|
+
@fork_writer.close
|
265
|
+
end
|
254
266
|
|
255
267
|
Thread.new do
|
268
|
+
Puma.set_thread_name "worker check pipe"
|
256
269
|
IO.select [@check_pipe]
|
257
270
|
log "! Detected parent died, dying"
|
258
271
|
exit! 1
|
@@ -270,16 +283,49 @@ module Puma
|
|
270
283
|
|
271
284
|
# Invoke any worker boot hooks so they can get
|
272
285
|
# things in shape before booting the app.
|
273
|
-
@launcher.config.run_hooks :before_worker_boot, index
|
286
|
+
@launcher.config.run_hooks :before_worker_boot, index, @launcher.events
|
287
|
+
|
288
|
+
server = @server ||= start_server
|
289
|
+
restart_server = Queue.new << true << false
|
274
290
|
|
275
|
-
|
291
|
+
if fork_worker
|
292
|
+
restart_server.clear
|
293
|
+
worker_pids = []
|
294
|
+
Signal.trap "SIGCHLD" do
|
295
|
+
wakeup! if worker_pids.reject! do |p|
|
296
|
+
Process.wait(p, Process::WNOHANG) rescue true
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
Thread.new do
|
301
|
+
Puma.set_thread_name "worker fork pipe"
|
302
|
+
while (idx = @fork_pipe.gets)
|
303
|
+
idx = idx.to_i
|
304
|
+
if idx == -1 # stop server
|
305
|
+
if restart_server.length > 0
|
306
|
+
restart_server.clear
|
307
|
+
server.begin_restart(true)
|
308
|
+
@launcher.config.run_hooks :before_refork, nil, @launcher.events
|
309
|
+
nakayoshi_gc
|
310
|
+
end
|
311
|
+
elsif idx == 0 # restart server
|
312
|
+
restart_server << true << false
|
313
|
+
else # fork worker
|
314
|
+
worker_pids << pid = spawn_worker(idx, master)
|
315
|
+
@worker_write << "f#{pid}:#{idx}\n" rescue nil
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
276
320
|
|
277
321
|
Signal.trap "SIGTERM" do
|
322
|
+
@worker_write << "e#{Process.pid}\n" rescue nil
|
278
323
|
server.stop
|
324
|
+
restart_server << false
|
279
325
|
end
|
280
326
|
|
281
327
|
begin
|
282
|
-
@worker_write << "b#{Process.pid}\n"
|
328
|
+
@worker_write << "b#{Process.pid}:#{index}\n"
|
283
329
|
rescue SystemCallError, IOError
|
284
330
|
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
285
331
|
STDERR.puts "Master seems to have exited, exiting."
|
@@ -287,17 +333,13 @@ module Puma
|
|
287
333
|
end
|
288
334
|
|
289
335
|
Thread.new(@worker_write) do |io|
|
290
|
-
|
336
|
+
Puma.set_thread_name "stat payload"
|
291
337
|
|
292
338
|
while true
|
293
339
|
sleep Const::WORKER_CHECK_INTERVAL
|
294
340
|
begin
|
295
|
-
|
296
|
-
|
297
|
-
t = server.pool_capacity || 0
|
298
|
-
m = server.max_threads || 0
|
299
|
-
payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
|
300
|
-
io << payload
|
341
|
+
require 'json'
|
342
|
+
io << "p#{Process.pid}#{server.stats.to_json}\n"
|
301
343
|
rescue IOError
|
302
344
|
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
303
345
|
break
|
@@ -305,11 +347,11 @@ module Puma
|
|
305
347
|
end
|
306
348
|
end
|
307
349
|
|
308
|
-
server.run.join
|
350
|
+
server.run.join while restart_server.pop
|
309
351
|
|
310
352
|
# Invoke any worker shutdown hooks so they can prevent the worker
|
311
353
|
# exiting until any background operations are completed
|
312
|
-
@launcher.config.run_hooks :before_worker_shutdown, index
|
354
|
+
@launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
|
313
355
|
ensure
|
314
356
|
@worker_write << "t#{Process.pid}\n" rescue nil
|
315
357
|
@worker_write.close
|
@@ -352,20 +394,62 @@ module Puma
|
|
352
394
|
Dir.chdir dir
|
353
395
|
end
|
354
396
|
|
397
|
+
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
398
|
+
# the master process.
|
355
399
|
def stats
|
356
400
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
357
|
-
|
358
|
-
|
359
|
-
|
401
|
+
worker_status = @workers.map do |w|
|
402
|
+
{
|
403
|
+
started_at: w.started_at.utc.iso8601,
|
404
|
+
pid: w.pid,
|
405
|
+
index: w.index,
|
406
|
+
phase: w.phase,
|
407
|
+
booted: w.booted?,
|
408
|
+
last_checkin: w.last_checkin.utc.iso8601,
|
409
|
+
last_status: w.last_status,
|
410
|
+
}
|
411
|
+
end
|
412
|
+
|
413
|
+
{
|
414
|
+
started_at: @started_at.utc.iso8601,
|
415
|
+
workers: @workers.size,
|
416
|
+
phase: @phase,
|
417
|
+
booted_workers: worker_status.count { |w| w[:booted] },
|
418
|
+
old_workers: old_worker_count,
|
419
|
+
worker_status: worker_status,
|
420
|
+
}
|
360
421
|
end
|
361
422
|
|
362
423
|
def preload?
|
363
424
|
@options[:preload_app]
|
364
425
|
end
|
365
426
|
|
427
|
+
# @version 5.0.0
|
428
|
+
def fork_worker!
|
429
|
+
if (worker = @workers.find { |w| w.index == 0 })
|
430
|
+
worker.phase += 1
|
431
|
+
end
|
432
|
+
phased_restart
|
433
|
+
end
|
434
|
+
|
366
435
|
# We do this in a separate method to keep the lambda scope
|
367
436
|
# of the signals handlers as small as possible.
|
368
437
|
def setup_signals
|
438
|
+
if @options[:fork_worker]
|
439
|
+
Signal.trap "SIGURG" do
|
440
|
+
fork_worker!
|
441
|
+
end
|
442
|
+
|
443
|
+
# Auto-fork after the specified number of requests.
|
444
|
+
if (fork_requests = @options[:fork_worker].to_i) > 0
|
445
|
+
@launcher.events.register(:ping!) do |w|
|
446
|
+
fork_worker! if w.index == 0 &&
|
447
|
+
w.phase == 0 &&
|
448
|
+
w.last_status[:requests_count] >= fork_requests
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
369
453
|
Signal.trap "SIGCHLD" do
|
370
454
|
wakeup!
|
371
455
|
end
|
@@ -449,12 +533,11 @@ module Puma
|
|
449
533
|
#
|
450
534
|
@check_pipe, @suicide_pipe = Puma::Util.pipe
|
451
535
|
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
end
|
536
|
+
# Separate pipe used by worker 0 to receive commands to
|
537
|
+
# fork new worker processes.
|
538
|
+
@fork_pipe, @fork_writer = Puma::Util.pipe
|
539
|
+
|
540
|
+
log "Use Ctrl-C to stop"
|
458
541
|
|
459
542
|
redirect_io
|
460
543
|
|
@@ -466,7 +549,8 @@ module Puma
|
|
466
549
|
|
467
550
|
@master_read, @worker_write = read, @wakeup
|
468
551
|
|
469
|
-
@launcher.config.run_hooks :before_fork, nil
|
552
|
+
@launcher.config.run_hooks :before_fork, nil, @launcher.events
|
553
|
+
nakayoshi_gc
|
470
554
|
|
471
555
|
spawn_workers
|
472
556
|
|
@@ -477,8 +561,6 @@ module Puma
|
|
477
561
|
@launcher.events.fire_on_booted!
|
478
562
|
|
479
563
|
begin
|
480
|
-
force_check = false
|
481
|
-
|
482
564
|
while @status == :run
|
483
565
|
begin
|
484
566
|
if @phased_restart
|
@@ -486,31 +568,39 @@ module Puma
|
|
486
568
|
@phased_restart = false
|
487
569
|
end
|
488
570
|
|
489
|
-
check_workers
|
490
|
-
|
491
|
-
force_check = false
|
571
|
+
check_workers
|
492
572
|
|
493
|
-
res = IO.select([read], nil, nil,
|
573
|
+
res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
|
494
574
|
|
495
575
|
if res
|
496
576
|
req = read.read_nonblock(1)
|
497
577
|
|
578
|
+
@next_check = Time.now if req == "!"
|
498
579
|
next if !req || req == "!"
|
499
580
|
|
500
581
|
result = read.gets
|
501
582
|
pid = result.to_i
|
502
583
|
|
584
|
+
if req == "b" || req == "f"
|
585
|
+
pid, idx = result.split(':').map(&:to_i)
|
586
|
+
w = @workers.find {|x| x.index == idx}
|
587
|
+
w.pid = pid if w.pid.nil?
|
588
|
+
end
|
589
|
+
|
503
590
|
if w = @workers.find { |x| x.pid == pid }
|
504
591
|
case req
|
505
592
|
when "b"
|
506
593
|
w.boot!
|
507
594
|
log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
|
508
|
-
|
595
|
+
@next_check = Time.now
|
596
|
+
when "e"
|
597
|
+
# external term, see worker method, Signal.trap "SIGTERM"
|
598
|
+
w.instance_variable_set :@term, true
|
509
599
|
when "t"
|
510
|
-
w.
|
511
|
-
force_check = true
|
600
|
+
w.term unless w.term?
|
512
601
|
when "p"
|
513
602
|
w.ping!(result.sub(/^\d+/,'').chomp)
|
603
|
+
@launcher.events.fire(:ping!, w)
|
514
604
|
end
|
515
605
|
else
|
516
606
|
log "! Out-of-sync worker list, no #{pid} worker"
|
@@ -537,6 +627,7 @@ module Puma
|
|
537
627
|
# `#term` if needed
|
538
628
|
def wait_workers
|
539
629
|
@workers.reject! do |w|
|
630
|
+
next false if w.pid.nil?
|
540
631
|
begin
|
541
632
|
if Process.wait(w.pid, Process::WNOHANG)
|
542
633
|
true
|
@@ -545,9 +636,36 @@ module Puma
|
|
545
636
|
nil
|
546
637
|
end
|
547
638
|
rescue Errno::ECHILD
|
548
|
-
|
639
|
+
begin
|
640
|
+
Process.kill(0, w.pid)
|
641
|
+
false # child still alive, but has another parent
|
642
|
+
rescue Errno::ESRCH, Errno::EPERM
|
643
|
+
true # child is already terminated
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
# @version 5.0.0
|
650
|
+
def timeout_workers
|
651
|
+
@workers.each do |w|
|
652
|
+
if !w.term? && w.ping_timeout <= Time.now
|
653
|
+
log "! Terminating timed out worker: #{w.pid}"
|
654
|
+
w.kill
|
549
655
|
end
|
550
656
|
end
|
551
657
|
end
|
658
|
+
|
659
|
+
# @version 5.0.0
|
660
|
+
def nakayoshi_gc
|
661
|
+
return unless @options[:nakayoshi_fork]
|
662
|
+
log "! Promoting existing objects to old generation..."
|
663
|
+
4.times { GC.start(full_mark: false) }
|
664
|
+
if GC.respond_to?(:compact)
|
665
|
+
log "! Compacting..."
|
666
|
+
GC.compact
|
667
|
+
end
|
668
|
+
log "! Friendly fork preparation complete."
|
669
|
+
end
|
552
670
|
end
|
553
671
|
end
|