puma 6.4.3 → 6.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/puma/cluster.rb CHANGED
@@ -44,10 +44,15 @@ module Puma
44
44
  end
45
45
  end
46
46
 
47
- def start_phased_restart
47
+ def start_phased_restart(refork = false)
48
48
  @events.fire_on_restart!
49
+
49
50
  @phase += 1
50
- log "- Starting phased worker restart, phase: #{@phase}"
51
+ if refork
52
+ log "- Starting worker refork, phase: #{@phase}"
53
+ else
54
+ log "- Starting phased worker restart, phase: #{@phase}"
55
+ end
51
56
 
52
57
  # Be sure to change the directory again before loading
53
58
  # the app. This way we can pick up new code.
@@ -87,6 +92,10 @@ module Puma
87
92
 
88
93
  if @options[:fork_worker] && all_workers_in_phase?
89
94
  @fork_writer << "0\n"
95
+
96
+ if worker_at(0).phase > 0
97
+ @fork_writer << "-2\n"
98
+ end
90
99
  end
91
100
  end
92
101
 
@@ -162,7 +171,7 @@ module Puma
162
171
  (@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
163
172
  end
164
173
 
165
- def check_workers
174
+ def check_workers(refork = false)
166
175
  return if @next_check >= Time.now
167
176
 
168
177
  @next_check = Time.now + @options[:worker_check_interval]
@@ -180,7 +189,12 @@ module Puma
180
189
  w = @workers.find { |x| x.phase != @phase }
181
190
 
182
191
  if w
183
- log "- Stopping #{w.pid} for phased upgrade..."
192
+ if refork
193
+ log "- Stopping #{w.pid} for refork..."
194
+ else
195
+ log "- Stopping #{w.pid} for phased upgrade..."
196
+ end
197
+
184
198
  unless w.term?
185
199
  w.term
186
200
  log "- #{w.signal} sent to #{w.pid}..."
@@ -224,7 +238,7 @@ module Puma
224
238
  def phased_restart(refork = false)
225
239
  return false if @options[:preload_app] && !refork
226
240
 
227
- @phased_restart = true
241
+ @phased_restart = refork ? :refork : true
228
242
  wakeup!
229
243
 
230
244
  true
@@ -348,8 +362,6 @@ module Puma
348
362
  def run
349
363
  @status = :run
350
364
 
351
- @idle_workers = {}
352
-
353
365
  output_header "cluster"
354
366
 
355
367
  # This is aligned with the output from Runner, see Runner#output_header
@@ -366,7 +378,7 @@ module Puma
366
378
 
367
379
  before = Thread.list.reject(&fork_safe)
368
380
 
369
- log "* Restarts: (\u2714) hot (\u2716) phased"
381
+ log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
370
382
  log "* Preloading application"
371
383
  load_and_bind
372
384
 
@@ -384,7 +396,7 @@ module Puma
384
396
  end
385
397
  end
386
398
  else
387
- log "* Restarts: (\u2714) hot (\u2714) phased"
399
+ log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
388
400
 
389
401
  unless @config.app_configured?
390
402
  error "No application configured, nothing to run"
@@ -440,30 +452,37 @@ module Puma
440
452
 
441
453
  while @status == :run
442
454
  begin
443
- if all_workers_idle_timed_out?
455
+ if @options[:idle_timeout] && all_workers_idle_timed_out?
444
456
  log "- All workers reached idle timeout"
445
457
  break
446
458
  end
447
459
 
448
460
  if @phased_restart
449
- start_phased_restart
461
+ start_phased_restart(@phased_restart == :refork)
462
+
463
+ in_phased_restart = @phased_restart
450
464
  @phased_restart = false
451
- in_phased_restart = true
465
+
452
466
  workers_not_booted = @options[:workers]
467
+ # worker 0 is not restarted on refork
468
+ workers_not_booted -= 1 if in_phased_restart == :refork
453
469
  end
454
470
 
455
- check_workers
471
+ check_workers(in_phased_restart == :refork)
456
472
 
457
473
  if read.wait_readable([0, @next_check - Time.now].max)
458
474
  req = read.read_nonblock(1)
475
+ next unless req
459
476
 
460
- @next_check = Time.now if req == "!"
461
- next if !req || req == "!"
477
+ if req == PIPE_WAKEUP
478
+ @next_check = Time.now
479
+ next
480
+ end
462
481
 
463
482
  result = read.gets
464
483
  pid = result.to_i
465
484
 
466
- if req == "b" || req == "f"
485
+ if req == PIPE_BOOT || req == PIPE_FORK
467
486
  pid, idx = result.split(':').map(&:to_i)
468
487
  w = worker_at idx
469
488
  w.pid = pid if w.pid.nil?
@@ -471,22 +490,22 @@ module Puma
471
490
 
472
491
  if w = @workers.find { |x| x.pid == pid }
473
492
  case req
474
- when "b"
493
+ when PIPE_BOOT
475
494
  w.boot!
476
495
  log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
477
496
  @next_check = Time.now
478
497
  workers_not_booted -= 1
479
- when "e"
498
+ when PIPE_EXTERNAL_TERM
480
499
  # external term, see worker method, Signal.trap "SIGTERM"
481
500
  w.term!
482
- when "t"
501
+ when PIPE_TERM
483
502
  w.term unless w.term?
484
- when "p"
503
+ when PIPE_PING
485
504
  status = result.sub(/^\d+/,'').chomp
486
505
  w.ping!(status)
487
506
  @events.fire(:ping!, w)
488
507
 
489
- if in_phased_restart && workers_not_booted.positive? && w0 = worker_at(0)
508
+ if in_phased_restart && @options[:fork_worker] && workers_not_booted.positive? && w0 = worker_at(0)
490
509
  w0.ping!(status)
491
510
  @events.fire(:ping!, w0)
492
511
  end
@@ -496,11 +515,11 @@ module Puma
496
515
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
497
516
  booted = true
498
517
  end
499
- when "i"
500
- if @idle_workers[pid]
501
- @idle_workers.delete pid
518
+ when PIPE_IDLE
519
+ if idle_workers[pid]
520
+ idle_workers.delete pid
502
521
  else
503
- @idle_workers[pid] = true
522
+ idle_workers[pid] = true
504
523
  end
505
524
  end
506
525
  else
@@ -560,9 +579,12 @@ module Puma
560
579
  @workers.reject! do |w|
561
580
  next false if w.pid.nil?
562
581
  begin
563
- # When `fork_worker` is enabled, some worker may not be direct children, but grand children.
564
- # Because of this they won't be reaped by `Process.wait2(-1)`, so we need to check them individually)
565
- if reaped_children.delete(w.pid) || (@options[:fork_worker] && Process.wait(w.pid, Process::WNOHANG))
582
+ # We may need to check the PID individually because:
583
+ # 1. From Ruby versions 2.6 to 3.2, `Process.detach` can prevent or delay
584
+ # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
585
+ # 2. When `fork_worker` is enabled, some worker may not be direct children,
586
+ # but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
587
+ if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
566
588
  true
567
589
  else
568
590
  w.term if w.term?
@@ -602,7 +624,11 @@ module Puma
602
624
  end
603
625
 
604
626
  def idle_timed_out_worker_pids
605
- @idle_workers.keys
627
+ idle_workers.keys
628
+ end
629
+
630
+ def idle_workers
631
+ @idle_workers ||= {}
606
632
  end
607
633
  end
608
634
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'rack/builder'
4
3
  require_relative 'plugin'
5
4
  require_relative 'const'
6
5
  require_relative 'dsl'
@@ -131,6 +130,7 @@ module Puma
131
130
  binds: ['tcp://0.0.0.0:9292'.freeze],
132
131
  clean_thread_locals: false,
133
132
  debug: false,
133
+ enable_keep_alives: true,
134
134
  early_hints: nil,
135
135
  environment: 'development'.freeze,
136
136
  # Number of seconds to wait until we get the first data for the request.
@@ -173,8 +173,8 @@ module Puma
173
173
  http_content_length_limit: nil
174
174
  }
175
175
 
176
- def initialize(user_options={}, default_options = {}, &block)
177
- default_options = self.puma_default_options.merge(default_options)
176
+ def initialize(user_options={}, default_options = {}, env = ENV, &block)
177
+ default_options = self.puma_default_options(env).merge(default_options)
178
178
 
179
179
  @options = UserFileDefaultOptions.new(user_options, default_options)
180
180
  @plugins = PluginLoader.new
@@ -186,6 +186,8 @@ module Puma
186
186
  default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
187
187
  end
188
188
 
189
+ @puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'
190
+
189
191
  if block
190
192
  configure(&block)
191
193
  end
@@ -216,22 +218,27 @@ module Puma
216
218
  self
217
219
  end
218
220
 
219
- def puma_default_options
221
+ def puma_default_options(env = ENV)
220
222
  defaults = DEFAULTS.dup
221
- puma_options_from_env.each { |k,v| defaults[k] = v if v }
223
+ puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
222
224
  defaults
223
225
  end
224
226
 
225
- def puma_options_from_env
226
- min = ENV['PUMA_MIN_THREADS'] || ENV['MIN_THREADS']
227
- max = ENV['PUMA_MAX_THREADS'] || ENV['MAX_THREADS']
228
- workers = ENV['WEB_CONCURRENCY']
227
+ def puma_options_from_env(env = ENV)
228
+ min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
229
+ max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
230
+ workers = if env['WEB_CONCURRENCY'] == 'auto'
231
+ require_processor_counter
232
+ ::Concurrent.available_processor_count
233
+ else
234
+ env['WEB_CONCURRENCY']
235
+ end
229
236
 
230
237
  {
231
- min_threads: min && Integer(min),
232
- max_threads: max && Integer(max),
233
- workers: workers && Integer(workers),
234
- environment: ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV'],
238
+ min_threads: min && min != "" && Integer(min),
239
+ max_threads: max && max != "" && Integer(max),
240
+ workers: workers && workers != "" && Integer(workers),
241
+ environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
235
242
  }
236
243
  end
237
244
 
@@ -311,6 +318,8 @@ module Puma
311
318
  # @param arg [Launcher, Int] `:on_restart` passes Launcher
312
319
  #
313
320
  def run_hooks(key, arg, log_writer, hook_data = nil)
321
+ log_writer.debug "Running #{key} hooks"
322
+
314
323
  @options.all_of(key).each do |b|
315
324
  begin
316
325
  if Array === b
@@ -339,12 +348,22 @@ module Puma
339
348
 
340
349
  private
341
350
 
351
+ def require_processor_counter
352
+ require 'concurrent/utility/processor_counter'
353
+ rescue LoadError
354
+ warn <<~MESSAGE
355
+ WEB_CONCURRENCY=auto requires the "concurrent-ruby" gem to be installed.
356
+ Please add "concurrent-ruby" to your Gemfile.
357
+ MESSAGE
358
+ raise
359
+ end
360
+
342
361
  # Load and use the normal Rack builder if we can, otherwise
343
362
  # fallback to our minimal version.
344
363
  def rack_builder
345
364
  # Load bundler now if we can so that we can pickup rack from
346
365
  # a Gemfile
347
- if ENV.key? 'PUMA_BUNDLER_PRUNED'
366
+ if @puma_bundler_pruned
348
367
  begin
349
368
  require 'bundler/setup'
350
369
  rescue LoadError
@@ -354,11 +373,10 @@ module Puma
354
373
  begin
355
374
  require 'rack'
356
375
  require 'rack/builder'
376
+ ::Rack::Builder
357
377
  rescue LoadError
358
- # ok, use builtin version
359
- return Puma::Rack::Builder
360
- else
361
- return ::Rack::Builder
378
+ require_relative 'rack/builder'
379
+ Puma::Rack::Builder
362
380
  end
363
381
  end
364
382
 
data/lib/puma/const.rb CHANGED
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "6.4.3"
104
- CODE_NAME = "The Eagle of Durango"
103
+ PUMA_VERSION = VERSION = "6.6.1"
104
+ CODE_NAME = "Return to Forever"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
@@ -137,7 +137,7 @@ module Puma
137
137
  }.freeze
138
138
 
139
139
  # The basic max request size we'll try to read.
140
- CHUNK_SIZE = 16 * 1024
140
+ CHUNK_SIZE = 64 * 1024
141
141
 
142
142
  # This is the maximum header that is allowed before a client is booted. The parser detects
143
143
  # this, but we'd also like to do this as well.
@@ -293,5 +293,16 @@ module Puma
293
293
  BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
294
294
 
295
295
  PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
296
+
297
+ # All constants are prefixed with `PIPE_` to avoid name collisions.
298
+ module PipeRequest
299
+ PIPE_WAKEUP = "!"
300
+ PIPE_BOOT = "b"
301
+ PIPE_FORK = "f"
302
+ PIPE_EXTERNAL_TERM = "e"
303
+ PIPE_TERM = "t"
304
+ PIPE_PING = "p"
305
+ PIPE_IDLE = "i"
306
+ end
296
307
  end
297
308
  end
@@ -37,7 +37,7 @@ module Puma
37
37
  # @version 5.0.0
38
38
  PRINTABLE_COMMANDS = %w[gc-stats stats thread-backtraces].freeze
39
39
 
40
- def initialize(argv, stdout=STDOUT, stderr=STDERR)
40
+ def initialize(argv, stdout=STDOUT, stderr=STDERR, env: ENV)
41
41
  @state = nil
42
42
  @quiet = false
43
43
  @pidfile = nil
@@ -46,7 +46,7 @@ module Puma
46
46
  @control_auth_token = nil
47
47
  @config_file = nil
48
48
  @command = nil
49
- @environment = ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV']
49
+ @environment = env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV']
50
50
 
51
51
  @argv = argv.dup
52
52
  @stdout = stdout
@@ -60,7 +60,7 @@ module Puma
60
60
  @state = arg
61
61
  end
62
62
 
63
- o.on "-Q", "--quiet", "Not display messages" do |arg|
63
+ o.on "-Q", "--quiet", "Do not display messages" do |arg|
64
64
  @quiet = true
65
65
  end
66
66
 
@@ -127,7 +127,7 @@ module Puma
127
127
  require_relative 'configuration'
128
128
  require_relative 'log_writer'
129
129
 
130
- config = Puma::Configuration.new({ config_files: [@config_file] }, {})
130
+ config = Puma::Configuration.new({ config_files: [@config_file] }, {} , env)
131
131
  config.load
132
132
  @state ||= config.options[:state]
133
133
  @control_url ||= config.options[:control_url]