puma 4.3.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +94 -3
  3. data/LICENSE +23 -20
  4. data/README.md +26 -13
  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/signals.md +7 -6
  16. data/docs/systemd.md +1 -63
  17. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  18. data/ext/puma_http11/extconf.rb +4 -3
  19. data/ext/puma_http11/http11_parser.c +3 -1
  20. data/ext/puma_http11/http11_parser.rl +3 -1
  21. data/ext/puma_http11/mini_ssl.c +15 -2
  22. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  23. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  24. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  25. data/ext/puma_http11/puma_http11.c +7 -38
  26. data/lib/puma.rb +17 -0
  27. data/lib/puma/app/status.rb +18 -3
  28. data/lib/puma/binder.rb +88 -68
  29. data/lib/puma/cli.rb +7 -15
  30. data/lib/puma/client.rb +67 -14
  31. data/lib/puma/cluster.rb +191 -74
  32. data/lib/puma/commonlogger.rb +2 -2
  33. data/lib/puma/configuration.rb +31 -42
  34. data/lib/puma/const.rb +4 -3
  35. data/lib/puma/control_cli.rb +29 -17
  36. data/lib/puma/detect.rb +17 -0
  37. data/lib/puma/dsl.rb +144 -70
  38. data/lib/puma/error_logger.rb +97 -0
  39. data/lib/puma/events.rb +35 -31
  40. data/lib/puma/io_buffer.rb +9 -2
  41. data/lib/puma/jruby_restart.rb +0 -58
  42. data/lib/puma/launcher.rb +49 -31
  43. data/lib/puma/minissl.rb +60 -18
  44. data/lib/puma/minissl/context_builder.rb +0 -3
  45. data/lib/puma/null_io.rb +1 -1
  46. data/lib/puma/plugin.rb +1 -10
  47. data/lib/puma/rack/builder.rb +0 -4
  48. data/lib/puma/reactor.rb +9 -4
  49. data/lib/puma/runner.rb +8 -36
  50. data/lib/puma/server.rb +149 -186
  51. data/lib/puma/single.rb +7 -64
  52. data/lib/puma/state_file.rb +6 -3
  53. data/lib/puma/thread_pool.rb +94 -49
  54. data/lib/rack/handler/puma.rb +1 -3
  55. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  56. metadata +21 -23
  57. data/docs/tcp_mode.md +0 -96
  58. data/ext/puma_http11/io_buffer.c +0 -155
  59. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  60. data/lib/puma/tcp_logger.rb +0 -41
  61. data/tools/jungle/README.md +0 -19
  62. data/tools/jungle/init.d/README.md +0 -61
  63. data/tools/jungle/init.d/puma +0 -421
  64. 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,12 +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 = '{}'
75
+ @last_status = {}
77
76
  @term = false
78
77
  end
79
78
 
80
79
  attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
81
80
 
81
+ # @version 5.0.0
82
+ attr_writer :pid, :phase
83
+
82
84
  def booted?
83
85
  @stage == :booted
84
86
  end
@@ -94,11 +96,18 @@ module Puma
94
96
 
95
97
  def ping!(status)
96
98
  @last_checkin = Time.now
97
- @last_status = status
99
+ require 'json'
100
+ @last_status = JSON.parse(status, symbolize_names: true)
98
101
  end
99
102
 
100
- def ping_timeout?(which)
101
- 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
+ )
102
111
  end
103
112
 
104
113
  def term
@@ -109,14 +118,14 @@ module Puma
109
118
  @term ||= true
110
119
  @first_term_sent ||= Time.now
111
120
  end
112
- Process.kill @signal, @pid
121
+ Process.kill @signal, @pid if @pid
113
122
  rescue Errno::ESRCH
114
123
  end
115
124
  end
116
125
 
117
126
  def kill
118
- Process.kill "KILL", @pid
119
- rescue Errno::ESRCH
127
+ @signal = 'KILL'
128
+ term
120
129
  end
121
130
 
122
131
  def hup
@@ -130,27 +139,44 @@ module Puma
130
139
  return if diff < 1
131
140
 
132
141
  master = Process.pid
142
+ if @options[:fork_worker]
143
+ @fork_writer << "-1\n"
144
+ end
133
145
 
134
146
  diff.times do
135
147
  idx = next_worker_index
136
- @launcher.config.run_hooks :before_worker_fork, idx
137
148
 
138
- pid = fork { worker(idx, master) }
139
- if !pid
140
- log "! Complete inability to spawn new workers detected"
141
- log "! Seppuku is the only choice."
142
- 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)
143
154
  end
144
155
 
145
156
  debug "Spawned worker: #{pid}"
146
157
  @workers << Worker.new(idx, pid, @phase, @options)
158
+ end
159
+
160
+ if @options[:fork_worker] &&
161
+ @workers.all? {|x| x.phase == @phase}
147
162
 
148
- @launcher.config.run_hooks :after_worker_fork, idx
163
+ @fork_writer << "0\n"
149
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
150
170
 
151
- if diff > 0
152
- @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
153
176
  end
177
+
178
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
179
+ pid
154
180
  end
155
181
 
156
182
  def cull_workers
@@ -179,26 +205,12 @@ module Puma
179
205
  @workers.count { |w| !w.booted? } == 0
180
206
  end
181
207
 
182
- def check_workers(force=false)
183
- return if !force && @next_check && @next_check >= Time.now
208
+ def check_workers
209
+ return if @next_check >= Time.now
184
210
 
185
211
  @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
186
212
 
187
- any = false
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
-
213
+ timeout_workers
202
214
  wait_workers
203
215
  cull_workers
204
216
  spawn_workers
@@ -211,17 +223,18 @@ module Puma
211
223
  w = @workers.find { |x| x.phase != @phase }
212
224
 
213
225
  if w
214
- if @phased_state == :idle
215
- @phased_state = :waiting
216
- log "- Stopping #{w.pid} for phased upgrade..."
217
- end
218
-
226
+ log "- Stopping #{w.pid} for phased upgrade..."
219
227
  unless w.term?
220
228
  w.term
221
229
  log "- #{w.signal} sent to #{w.pid}..."
222
230
  end
223
231
  end
224
232
  end
233
+
234
+ @next_check = [
235
+ @workers.reject(&:term?).map(&:ping_timeout).min,
236
+ @next_check
237
+ ].compact.min
225
238
  end
226
239
 
227
240
  def wakeup!
@@ -240,10 +253,16 @@ module Puma
240
253
  $0 = title
241
254
 
242
255
  Signal.trap "SIGINT", "IGNORE"
256
+ Signal.trap "SIGCHLD", "DEFAULT"
257
+
258
+ fork_worker = @options[:fork_worker] && index == 0
243
259
 
244
260
  @workers = []
245
- @master_read.close
246
- @suicide_pipe.close
261
+ if !@options[:fork_worker] || fork_worker
262
+ @master_read.close
263
+ @suicide_pipe.close
264
+ @fork_writer.close
265
+ end
247
266
 
248
267
  Thread.new do
249
268
  Puma.set_thread_name "worker check pipe"
@@ -264,17 +283,49 @@ module Puma
264
283
 
265
284
  # Invoke any worker boot hooks so they can get
266
285
  # things in shape before booting the app.
267
- @launcher.config.run_hooks :before_worker_boot, index
286
+ @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
268
287
 
269
- server = start_server
288
+ server = @server ||= start_server
289
+ restart_server = Queue.new << true << false
290
+
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
270
320
 
271
321
  Signal.trap "SIGTERM" do
272
322
  @worker_write << "e#{Process.pid}\n" rescue nil
273
323
  server.stop
324
+ restart_server << false
274
325
  end
275
326
 
276
327
  begin
277
- @worker_write << "b#{Process.pid}\n"
328
+ @worker_write << "b#{Process.pid}:#{index}\n"
278
329
  rescue SystemCallError, IOError
279
330
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
280
331
  STDERR.puts "Master seems to have exited, exiting."
@@ -283,17 +334,12 @@ module Puma
283
334
 
284
335
  Thread.new(@worker_write) do |io|
285
336
  Puma.set_thread_name "stat payload"
286
- base_payload = "p#{Process.pid}"
287
337
 
288
338
  while true
289
339
  sleep Const::WORKER_CHECK_INTERVAL
290
340
  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
341
+ require 'json'
342
+ io << "p#{Process.pid}#{server.stats.to_json}\n"
297
343
  rescue IOError
298
344
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
299
345
  break
@@ -301,11 +347,11 @@ module Puma
301
347
  end
302
348
  end
303
349
 
304
- server.run.join
350
+ server.run.join while restart_server.pop
305
351
 
306
352
  # Invoke any worker shutdown hooks so they can prevent the worker
307
353
  # exiting until any background operations are completed
308
- @launcher.config.run_hooks :before_worker_shutdown, index
354
+ @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
309
355
  ensure
310
356
  @worker_write << "t#{Process.pid}\n" rescue nil
311
357
  @worker_write.close
@@ -352,18 +398,58 @@ module Puma
352
398
  # the master process.
353
399
  def stats
354
400
  old_worker_count = @workers.count { |w| w.phase != @phase }
355
- booted_worker_count = @workers.count { |w| w.booted? }
356
- 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(",") + ']'
357
- %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
+ }
358
421
  end
359
422
 
360
423
  def preload?
361
424
  @options[:preload_app]
362
425
  end
363
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
+
364
435
  # We do this in a separate method to keep the lambda scope
365
436
  # of the signals handlers as small as possible.
366
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
+
367
453
  Signal.trap "SIGCHLD" do
368
454
  wakeup!
369
455
  end
@@ -447,12 +533,11 @@ module Puma
447
533
  #
448
534
  @check_pipe, @suicide_pipe = Puma::Util.pipe
449
535
 
450
- if daemon?
451
- log "* Daemonizing..."
452
- Process.daemon(true)
453
- else
454
- log "Use Ctrl-C to stop"
455
- 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"
456
541
 
457
542
  redirect_io
458
543
 
@@ -464,7 +549,8 @@ module Puma
464
549
 
465
550
  @master_read, @worker_write = read, @wakeup
466
551
 
467
- @launcher.config.run_hooks :before_fork, nil
552
+ @launcher.config.run_hooks :before_fork, nil, @launcher.events
553
+ nakayoshi_gc
468
554
 
469
555
  spawn_workers
470
556
 
@@ -475,8 +561,6 @@ module Puma
475
561
  @launcher.events.fire_on_booted!
476
562
 
477
563
  begin
478
- force_check = false
479
-
480
564
  while @status == :run
481
565
  begin
482
566
  if @phased_restart
@@ -484,34 +568,39 @@ module Puma
484
568
  @phased_restart = false
485
569
  end
486
570
 
487
- check_workers force_check
571
+ check_workers
488
572
 
489
- force_check = false
490
-
491
- res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
573
+ res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
492
574
 
493
575
  if res
494
576
  req = read.read_nonblock(1)
495
577
 
578
+ @next_check = Time.now if req == "!"
496
579
  next if !req || req == "!"
497
580
 
498
581
  result = read.gets
499
582
  pid = result.to_i
500
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
+
501
590
  if w = @workers.find { |x| x.pid == pid }
502
591
  case req
503
592
  when "b"
504
593
  w.boot!
505
594
  log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
506
- force_check = true
595
+ @next_check = Time.now
507
596
  when "e"
508
597
  # external term, see worker method, Signal.trap "SIGTERM"
509
598
  w.instance_variable_set :@term, true
510
599
  when "t"
511
600
  w.term unless w.term?
512
- force_check = true
513
601
  when "p"
514
602
  w.ping!(result.sub(/^\d+/,'').chomp)
603
+ @launcher.events.fire(:ping!, w)
515
604
  end
516
605
  else
517
606
  log "! Out-of-sync worker list, no #{pid} worker"
@@ -538,6 +627,7 @@ module Puma
538
627
  # `#term` if needed
539
628
  def wait_workers
540
629
  @workers.reject! do |w|
630
+ next false if w.pid.nil?
541
631
  begin
542
632
  if Process.wait(w.pid, Process::WNOHANG)
543
633
  true
@@ -546,9 +636,36 @@ module Puma
546
636
  nil
547
637
  end
548
638
  rescue Errno::ECHILD
549
- 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
550
645
  end
551
646
  end
552
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
655
+ end
656
+ end
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
553
670
  end
554
671
  end