puma 4.3.6 → 5.0.0.beta1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +60 -12
  3. data/LICENSE +23 -20
  4. data/README.md +17 -11
  5. data/docs/deployment.md +3 -1
  6. data/docs/fork_worker.md +31 -0
  7. data/docs/jungle/README.md +13 -0
  8. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  9. data/{tools → docs}/jungle/rc.d/puma +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  11. data/{tools → docs}/jungle/upstart/README.md +0 -0
  12. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  13. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  14. data/docs/signals.md +1 -0
  15. data/docs/systemd.md +1 -63
  16. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  17. data/ext/puma_http11/extconf.rb +4 -3
  18. data/ext/puma_http11/http11_parser.c +1 -3
  19. data/ext/puma_http11/http11_parser.rl +1 -3
  20. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  21. data/ext/puma_http11/puma_http11.c +2 -38
  22. data/lib/puma.rb +4 -0
  23. data/lib/puma/app/status.rb +16 -5
  24. data/lib/puma/binder.rb +62 -60
  25. data/lib/puma/cli.rb +7 -15
  26. data/lib/puma/client.rb +30 -15
  27. data/lib/puma/cluster.rb +179 -74
  28. data/lib/puma/configuration.rb +30 -42
  29. data/lib/puma/const.rb +2 -3
  30. data/lib/puma/control_cli.rb +27 -17
  31. data/lib/puma/detect.rb +8 -0
  32. data/lib/puma/dsl.rb +70 -34
  33. data/lib/puma/io_buffer.rb +9 -2
  34. data/lib/puma/jruby_restart.rb +0 -58
  35. data/lib/puma/launcher.rb +41 -29
  36. data/lib/puma/minissl.rb +13 -8
  37. data/lib/puma/null_io.rb +1 -1
  38. data/lib/puma/plugin.rb +1 -10
  39. data/lib/puma/rack/builder.rb +0 -4
  40. data/lib/puma/reactor.rb +6 -1
  41. data/lib/puma/runner.rb +5 -34
  42. data/lib/puma/server.rb +62 -177
  43. data/lib/puma/single.rb +7 -64
  44. data/lib/puma/state_file.rb +5 -2
  45. data/lib/puma/thread_pool.rb +85 -47
  46. data/lib/rack/handler/puma.rb +1 -3
  47. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  48. metadata +19 -23
  49. data/docs/tcp_mode.md +0 -96
  50. data/ext/puma_http11/io_buffer.c +0 -155
  51. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  52. data/lib/puma/tcp_logger.rb +0 -41
  53. data/tools/jungle/README.md +0 -19
  54. data/tools/jungle/init.d/README.md +0 -61
  55. data/tools/jungle/init.d/puma +0 -421
  56. data/tools/jungle/init.d/run-puma +0 -18
@@ -5,6 +5,7 @@ require 'puma/util'
5
5
  require 'puma/plugin'
6
6
 
7
7
  require 'time'
8
+ require 'json'
8
9
 
9
10
  module Puma
10
11
  # This class is instantiated by the `Puma::Launcher` and used
@@ -24,9 +25,8 @@ module Puma
24
25
 
25
26
  @phase = 0
26
27
  @workers = []
27
- @next_check = nil
28
+ @next_check = Time.now
28
29
 
29
- @phased_state = :idle
30
30
  @phased_restart = false
31
31
  end
32
32
 
@@ -37,7 +37,7 @@ module Puma
37
37
  begin
38
38
  loop do
39
39
  wait_workers
40
- break if @workers.empty?
40
+ break if @workers.reject {|w| w.pid.nil?}.empty?
41
41
  sleep 0.2
42
42
  end
43
43
  rescue Interrupt
@@ -73,11 +73,12 @@ module Puma
73
73
  @first_term_sent = nil
74
74
  @started_at = Time.now
75
75
  @last_checkin = Time.now
76
- @last_status = '{}'
76
+ @last_status = {}
77
77
  @term = false
78
78
  end
79
79
 
80
80
  attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
81
+ attr_writer :pid, :phase
81
82
 
82
83
  def booted?
83
84
  @stage == :booted
@@ -94,11 +95,15 @@ module Puma
94
95
 
95
96
  def ping!(status)
96
97
  @last_checkin = Time.now
97
- @last_status = status
98
+ @last_status = JSON.parse(status, symbolize_names: true)
98
99
  end
99
100
 
100
- def ping_timeout?(which)
101
- Time.now - @last_checkin > which
101
+ def ping_timeout
102
+ @last_checkin +
103
+ (booted? ?
104
+ @options[:worker_timeout] :
105
+ @options[:worker_boot_timeout]
106
+ )
102
107
  end
103
108
 
104
109
  def term
@@ -109,14 +114,14 @@ module Puma
109
114
  @term ||= true
110
115
  @first_term_sent ||= Time.now
111
116
  end
112
- Process.kill @signal, @pid
117
+ Process.kill @signal, @pid if @pid
113
118
  rescue Errno::ESRCH
114
119
  end
115
120
  end
116
121
 
117
122
  def kill
118
- Process.kill "KILL", @pid
119
- rescue Errno::ESRCH
123
+ @signal = 'KILL'
124
+ term
120
125
  end
121
126
 
122
127
  def hup
@@ -130,27 +135,43 @@ module Puma
130
135
  return if diff < 1
131
136
 
132
137
  master = Process.pid
138
+ if @options[:fork_worker]
139
+ @fork_writer << "-1\n"
140
+ end
133
141
 
134
142
  diff.times do
135
143
  idx = next_worker_index
136
- @launcher.config.run_hooks :before_worker_fork, idx
137
144
 
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
145
+ if @options[:fork_worker] && idx != 0
146
+ @fork_writer << "#{idx}\n"
147
+ pid = nil
148
+ else
149
+ pid = spawn_worker(idx, master)
143
150
  end
144
151
 
145
152
  debug "Spawned worker: #{pid}"
146
153
  @workers << Worker.new(idx, pid, @phase, @options)
154
+ end
147
155
 
148
- @launcher.config.run_hooks :after_worker_fork, idx
156
+ if @options[:fork_worker] &&
157
+ @workers.all? {|x| x.phase == @phase}
158
+
159
+ @fork_writer << "0\n"
149
160
  end
161
+ end
150
162
 
151
- if diff > 0
152
- @phased_state = :idle
163
+ def spawn_worker(idx, master)
164
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
165
+
166
+ pid = fork { worker(idx, master) }
167
+ if !pid
168
+ log "! Complete inability to spawn new workers detected"
169
+ log "! Seppuku is the only choice."
170
+ exit! 1
153
171
  end
172
+
173
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
174
+ pid
154
175
  end
155
176
 
156
177
  def cull_workers
@@ -179,26 +200,12 @@ module Puma
179
200
  @workers.count { |w| !w.booted? } == 0
180
201
  end
181
202
 
182
- def check_workers(force=false)
183
- return if !force && @next_check && @next_check >= Time.now
203
+ def check_workers
204
+ return if @next_check >= Time.now
184
205
 
185
206
  @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
186
207
 
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
-
208
+ timeout_workers
202
209
  wait_workers
203
210
  cull_workers
204
211
  spawn_workers
@@ -211,17 +218,18 @@ module Puma
211
218
  w = @workers.find { |x| x.phase != @phase }
212
219
 
213
220
  if w
214
- if @phased_state == :idle
215
- @phased_state = :waiting
216
- log "- Stopping #{w.pid} for phased upgrade..."
217
- end
218
-
221
+ log "- Stopping #{w.pid} for phased upgrade..."
219
222
  unless w.term?
220
223
  w.term
221
224
  log "- #{w.signal} sent to #{w.pid}..."
222
225
  end
223
226
  end
224
227
  end
228
+
229
+ @next_check = [
230
+ @workers.reject(&:term?).map(&:ping_timeout).min,
231
+ @next_check
232
+ ].compact.min
225
233
  end
226
234
 
227
235
  def wakeup!
@@ -241,9 +249,14 @@ module Puma
241
249
 
242
250
  Signal.trap "SIGINT", "IGNORE"
243
251
 
252
+ fork_worker = @options[:fork_worker] && index == 0
253
+
244
254
  @workers = []
245
- @master_read.close
246
- @suicide_pipe.close
255
+ if !@options[:fork_worker] || fork_worker
256
+ @master_read.close
257
+ @suicide_pipe.close
258
+ @fork_writer.close
259
+ end
247
260
 
248
261
  Thread.new do
249
262
  Puma.set_thread_name "worker check pipe"
@@ -264,17 +277,47 @@ module Puma
264
277
 
265
278
  # Invoke any worker boot hooks so they can get
266
279
  # things in shape before booting the app.
267
- @launcher.config.run_hooks :before_worker_boot, index
280
+ @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
281
+
282
+ server = @server ||= start_server
283
+ restart_server = Queue.new << true << false
268
284
 
269
- server = start_server
285
+ if fork_worker
286
+ restart_server.clear
287
+ Signal.trap "SIGCHLD" do
288
+ Process.wait(-1, Process::WNOHANG) rescue nil
289
+ wakeup!
290
+ end
291
+
292
+ Thread.new do
293
+ Puma.set_thread_name "worker fork pipe"
294
+ while (idx = @fork_pipe.gets)
295
+ idx = idx.to_i
296
+ if idx == -1 # stop server
297
+ if restart_server.length > 0
298
+ restart_server.clear
299
+ server.begin_restart(true)
300
+ @launcher.config.run_hooks :before_refork, nil, @launcher.events
301
+ nakayoshi_gc
302
+ end
303
+ elsif idx == 0 # restart server
304
+ restart_server << true << false
305
+ else # fork worker
306
+ pid = spawn_worker(idx, master)
307
+ @worker_write << "f#{pid}:#{idx}\n" rescue nil
308
+ end
309
+ end
310
+ end
311
+ end
270
312
 
271
313
  Signal.trap "SIGTERM" do
272
314
  @worker_write << "e#{Process.pid}\n" rescue nil
273
315
  server.stop
316
+ restart_server << false
274
317
  end
275
318
 
276
319
  begin
277
- @worker_write << "b#{Process.pid}\n"
320
+ @worker_write << "b#{Process.pid}:#{index}\n"
278
321
  rescue SystemCallError, IOError
279
322
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
280
323
  STDERR.puts "Master seems to have exited, exiting."
@@ -283,17 +326,11 @@ module Puma
283
326
 
284
327
  Thread.new(@worker_write) do |io|
285
328
  Puma.set_thread_name "stat payload"
286
- base_payload = "p#{Process.pid}"
287
329
 
288
330
  while true
289
331
  sleep Const::WORKER_CHECK_INTERVAL
290
332
  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
333
+ io << "p#{Process.pid}#{server.stats.to_json}\n"
297
334
  rescue IOError
298
335
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
299
336
  break
@@ -301,11 +338,11 @@ module Puma
301
338
  end
302
339
  end
303
340
 
304
- server.run.join
341
+ server.run.join while restart_server.pop
305
342
 
306
343
  # Invoke any worker shutdown hooks so they can prevent the worker
307
344
  # exiting until any background operations are completed
308
- @launcher.config.run_hooks :before_worker_shutdown, index
345
+ @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
309
346
  ensure
310
347
  @worker_write << "t#{Process.pid}\n" rescue nil
311
348
  @worker_write.close
@@ -352,18 +389,57 @@ module Puma
352
389
  # the master process.
353
390
  def stats
354
391
  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} }!
392
+ worker_status = @workers.map do |w|
393
+ {
394
+ started_at: w.started_at.utc.iso8601,
395
+ pid: w.pid,
396
+ index: w.index,
397
+ phase: w.phase,
398
+ booted: w.booted?,
399
+ last_checkin: w.last_checkin.utc.iso8601,
400
+ last_status: w.last_status,
401
+ }
402
+ end
403
+
404
+ {
405
+ started_at: @started_at.utc.iso8601,
406
+ workers: @workers.size,
407
+ phase: @phase,
408
+ booted_workers: worker_status.count { |w| w[:booted] },
409
+ old_workers: old_worker_count,
410
+ worker_status: worker_status,
411
+ }
358
412
  end
359
413
 
360
414
  def preload?
361
415
  @options[:preload_app]
362
416
  end
363
417
 
418
+ def fork_worker!
419
+ if (worker = @workers.find { |w| w.index == 0 })
420
+ worker.phase += 1
421
+ end
422
+ phased_restart
423
+ end
424
+
364
425
  # We do this in a separate method to keep the lambda scope
365
426
  # of the signals handlers as small as possible.
366
427
  def setup_signals
428
+ if @options[:fork_worker]
429
+ Signal.trap "SIGURG" do
430
+ fork_worker!
431
+ end
432
+
433
+ # Auto-fork after the specified number of requests.
434
+ if (fork_requests = @options[:fork_worker].to_i) > 0
435
+ @launcher.events.register(:ping!) do |w|
436
+ fork_worker! if w.index == 0 &&
437
+ w.phase == 0 &&
438
+ w.last_status[:requests_count] >= fork_requests
439
+ end
440
+ end
441
+ end
442
+
367
443
  Signal.trap "SIGCHLD" do
368
444
  wakeup!
369
445
  end
@@ -447,12 +523,11 @@ module Puma
447
523
  #
448
524
  @check_pipe, @suicide_pipe = Puma::Util.pipe
449
525
 
450
- if daemon?
451
- log "* Daemonizing..."
452
- Process.daemon(true)
453
- else
454
- log "Use Ctrl-C to stop"
455
- end
526
+ # Separate pipe used by worker 0 to receive commands to
527
+ # fork new worker processes.
528
+ @fork_pipe, @fork_writer = Puma::Util.pipe
529
+
530
+ log "Use Ctrl-C to stop"
456
531
 
457
532
  redirect_io
458
533
 
@@ -464,7 +539,8 @@ module Puma
464
539
 
465
540
  @master_read, @worker_write = read, @wakeup
466
541
 
467
- @launcher.config.run_hooks :before_fork, nil
542
+ @launcher.config.run_hooks :before_fork, nil, @launcher.events
543
+ nakayoshi_gc
468
544
 
469
545
  spawn_workers
470
546
 
@@ -475,8 +551,6 @@ module Puma
475
551
  @launcher.events.fire_on_booted!
476
552
 
477
553
  begin
478
- force_check = false
479
-
480
554
  while @status == :run
481
555
  begin
482
556
  if @phased_restart
@@ -484,34 +558,39 @@ module Puma
484
558
  @phased_restart = false
485
559
  end
486
560
 
487
- check_workers force_check
561
+ check_workers
488
562
 
489
- force_check = false
490
-
491
- res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
563
+ res = IO.select([read], nil, nil, [0, @next_check - Time.now].max)
492
564
 
493
565
  if res
494
566
  req = read.read_nonblock(1)
495
567
 
568
+ @next_check = Time.now if req == "!"
496
569
  next if !req || req == "!"
497
570
 
498
571
  result = read.gets
499
572
  pid = result.to_i
500
573
 
574
+ if req == "b" || req == "f"
575
+ pid, idx = result.split(':').map(&:to_i)
576
+ w = @workers.find {|x| x.index == idx}
577
+ w.pid = pid if w.pid.nil?
578
+ end
579
+
501
580
  if w = @workers.find { |x| x.pid == pid }
502
581
  case req
503
582
  when "b"
504
583
  w.boot!
505
584
  log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
506
- force_check = true
585
+ @next_check = Time.now
507
586
  when "e"
508
587
  # external term, see worker method, Signal.trap "SIGTERM"
509
588
  w.instance_variable_set :@term, true
510
589
  when "t"
511
590
  w.term unless w.term?
512
- force_check = true
513
591
  when "p"
514
592
  w.ping!(result.sub(/^\d+/,'').chomp)
593
+ @launcher.events.fire(:ping!, w)
515
594
  end
516
595
  else
517
596
  log "! Out-of-sync worker list, no #{pid} worker"
@@ -538,6 +617,7 @@ module Puma
538
617
  # `#term` if needed
539
618
  def wait_workers
540
619
  @workers.reject! do |w|
620
+ next false if w.pid.nil?
541
621
  begin
542
622
  if Process.wait(w.pid, Process::WNOHANG)
543
623
  true
@@ -546,9 +626,34 @@ module Puma
546
626
  nil
547
627
  end
548
628
  rescue Errno::ECHILD
549
- true # child is already terminated
629
+ begin
630
+ Process.kill(0, w.pid)
631
+ false # child still alive, but has another parent
632
+ rescue Errno::ESRCH, Errno::EPERM
633
+ true # child is already terminated
634
+ end
550
635
  end
551
636
  end
552
637
  end
638
+
639
+ def timeout_workers
640
+ @workers.each do |w|
641
+ if !w.term? && w.ping_timeout <= Time.now
642
+ log "! Terminating timed out worker: #{w.pid}"
643
+ w.kill
644
+ end
645
+ end
646
+ end
647
+
648
+ def nakayoshi_gc
649
+ return unless @options[:nakayoshi_fork]
650
+ log "! Promoting existing objects to old generation..."
651
+ 4.times { GC.start(full_mark: false) }
652
+ if GC.respond_to?(:compact)
653
+ log "! Compacting..."
654
+ GC.compact
655
+ end
656
+ log "! Friendly fork preparation complete."
657
+ end
553
658
  end
554
659
  end