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.

Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +149 -10
  3. data/LICENSE +23 -20
  4. data/README.md +30 -46
  5. data/docs/architecture.md +3 -3
  6. data/docs/deployment.md +9 -3
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/jungle/README.md +13 -0
  9. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma +0 -0
  11. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  12. data/{tools → docs}/jungle/upstart/README.md +0 -0
  13. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  14. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  15. data/docs/plugins.md +20 -10
  16. data/docs/signals.md +7 -6
  17. data/docs/systemd.md +1 -63
  18. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  19. data/ext/puma_http11/extconf.rb +6 -0
  20. data/ext/puma_http11/http11_parser.c +40 -63
  21. data/ext/puma_http11/http11_parser.java.rl +21 -37
  22. data/ext/puma_http11/http11_parser.rl +3 -1
  23. data/ext/puma_http11/http11_parser_common.rl +3 -3
  24. data/ext/puma_http11/mini_ssl.c +15 -2
  25. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  28. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  29. data/ext/puma_http11/puma_http11.c +9 -38
  30. data/lib/puma.rb +23 -0
  31. data/lib/puma/app/status.rb +46 -30
  32. data/lib/puma/binder.rb +112 -124
  33. data/lib/puma/cli.rb +11 -15
  34. data/lib/puma/client.rb +250 -209
  35. data/lib/puma/cluster.rb +203 -85
  36. data/lib/puma/commonlogger.rb +2 -2
  37. data/lib/puma/configuration.rb +31 -42
  38. data/lib/puma/const.rb +24 -19
  39. data/lib/puma/control_cli.rb +46 -17
  40. data/lib/puma/detect.rb +17 -0
  41. data/lib/puma/dsl.rb +162 -70
  42. data/lib/puma/error_logger.rb +97 -0
  43. data/lib/puma/events.rb +35 -31
  44. data/lib/puma/io_buffer.rb +9 -2
  45. data/lib/puma/jruby_restart.rb +0 -58
  46. data/lib/puma/launcher.rb +117 -58
  47. data/lib/puma/minissl.rb +60 -18
  48. data/lib/puma/minissl/context_builder.rb +73 -0
  49. data/lib/puma/null_io.rb +1 -1
  50. data/lib/puma/plugin.rb +6 -12
  51. data/lib/puma/rack/builder.rb +0 -4
  52. data/lib/puma/reactor.rb +16 -9
  53. data/lib/puma/runner.rb +11 -32
  54. data/lib/puma/server.rb +173 -193
  55. data/lib/puma/single.rb +7 -64
  56. data/lib/puma/state_file.rb +6 -3
  57. data/lib/puma/thread_pool.rb +104 -81
  58. data/lib/rack/handler/puma.rb +1 -5
  59. data/tools/Dockerfile +16 -0
  60. data/tools/trickletest.rb +0 -1
  61. metadata +23 -24
  62. data/ext/puma_http11/io_buffer.c +0 -155
  63. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  64. data/lib/puma/convenient.rb +0 -25
  65. data/lib/puma/daemon_ext.rb +0 -33
  66. data/lib/puma/delegation.rb +0 -13
  67. data/lib/puma/tcp_logger.rb +0 -41
  68. data/tools/jungle/README.md +0 -19
  69. data/tools/jungle/init.d/README.md +0 -61
  70. data/tools/jungle/init.d/puma +0 -421
  71. data/tools/jungle/init.d/run-puma +0 -18
@@ -24,9 +24,8 @@ module Puma
24
24
 
25
25
  @phase = 0
26
26
  @workers = []
27
- @next_check = nil
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
- @last_status = status
99
+ require 'json'
100
+ @last_status = JSON.parse(status, symbolize_names: true)
107
101
  end
108
102
 
109
- def ping_timeout?(which)
110
- Time.now - @last_checkin > which
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
- Process.kill "KILL", @pid
128
- rescue Errno::ESRCH
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
- pid = fork { worker(idx, master) }
148
- if !pid
149
- log "! Complete inability to spawn new workers detected"
150
- log "! Seppuku is the only choice."
151
- exit! 1
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
- @launcher.config.run_hooks :after_worker_fork, idx
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
- if diff > 0
161
- @phased_state = :idle
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(force=false)
192
- return if !force && @next_check && @next_check >= Time.now
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
- any = false
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
- if @phased_state == :idle
224
- @phased_state = :waiting
225
- log "- Stopping #{w.pid} for phased upgrade..."
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
- @master_read.close
253
- @suicide_pipe.close
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
- server = start_server
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
- base_payload = "p#{Process.pid}"
336
+ Puma.set_thread_name "stat payload"
291
337
 
292
338
  while true
293
339
  sleep Const::WORKER_CHECK_INTERVAL
294
340
  begin
295
- b = server.backlog || 0
296
- r = server.running || 0
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
- booted_worker_count = @workers.count { |w| w.booted? }
358
- worker_status = '[' + @workers.map { |w| %Q!{ "started_at": "#{w.started_at.utc.iso8601}", "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
359
- %Q!{ "started_at": "#{@started_at.utc.iso8601}", "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
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
- if daemon?
453
- log "* Daemonizing..."
454
- Process.daemon(true)
455
- else
456
- log "Use Ctrl-C to stop"
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 force_check
490
-
491
- force_check = false
571
+ check_workers
492
572
 
493
- res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
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
- force_check = true
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.dead!
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
- true # child is already terminated
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