puma 6.4.3-java → 6.5.0-java

Sign up to get free protection for your applications and to get access to all the features.
data/lib/puma/cli.rb CHANGED
@@ -24,7 +24,7 @@ module Puma
24
24
  # Create a new CLI object using +argv+ as the command line
25
25
  # arguments.
26
26
  #
27
- def initialize(argv, log_writer = LogWriter.stdio, events = Events.new)
27
+ def initialize(argv, log_writer = LogWriter.stdio, events = Events.new, env: ENV)
28
28
  @debug = false
29
29
  @argv = argv.dup
30
30
  @log_writer = log_writer
@@ -39,7 +39,7 @@ module Puma
39
39
  @control_url = nil
40
40
  @control_options = {}
41
41
 
42
- setup_options
42
+ setup_options env
43
43
 
44
44
  begin
45
45
  @parser.parse! @argv
@@ -63,7 +63,7 @@ module Puma
63
63
  end
64
64
  end
65
65
 
66
- @launcher = Puma::Launcher.new(@conf, :log_writer => @log_writer, :events => @events, :argv => argv)
66
+ @launcher = Puma::Launcher.new(@conf, env: ENV, log_writer: @log_writer, events: @events, argv: argv)
67
67
  end
68
68
 
69
69
  attr_reader :launcher
@@ -92,8 +92,8 @@ module Puma
92
92
  # Build the OptionParser object to handle the available options.
93
93
  #
94
94
 
95
- def setup_options
96
- @conf = Configuration.new({}, {events: @events}) do |user_config, file_config|
95
+ def setup_options(env = ENV)
96
+ @conf = Configuration.new({}, {events: @events}, env) do |user_config, file_config|
97
97
  @parser = OptionParser.new do |o|
98
98
  o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
99
99
  user_config.bind arg
@@ -157,6 +157,10 @@ module Puma
157
157
  user_config.pidfile arg
158
158
  end
159
159
 
160
+ o.on "--plugin PLUGIN", "Load the given PLUGIN. Can be used multiple times to load multiple plugins." do |arg|
161
+ user_config.plugin arg
162
+ end
163
+
160
164
  o.on "--preload", "Preload the app. Cluster mode only" do
161
165
  user_config.preload_app!
162
166
  end
data/lib/puma/client.rb CHANGED
@@ -160,8 +160,6 @@ module Puma
160
160
  @read_header = true
161
161
  @read_proxy = !!@expect_proxy_proto
162
162
  @env = @proto_env.dup
163
- @body = nil
164
- @tempfile = nil
165
163
  @parsed_bytes = 0
166
164
  @ready = false
167
165
  @body_remain = 0
@@ -190,11 +188,11 @@ module Puma
190
188
  rescue IOError
191
189
  # swallow it
192
190
  end
193
-
194
191
  end
195
192
  end
196
193
 
197
194
  def close
195
+ tempfile_close
198
196
  begin
199
197
  @io.close
200
198
  rescue IOError, Errno::EBADF
@@ -202,6 +200,15 @@ module Puma
202
200
  end
203
201
  end
204
202
 
203
+ def tempfile_close
204
+ tf_path = @tempfile&.path
205
+ @tempfile&.close
206
+ File.unlink(tf_path) if tf_path
207
+ @tempfile = nil
208
+ @body = nil
209
+ rescue Errno::ENOENT, IOError
210
+ end
211
+
205
212
  # If necessary, read the PROXY protocol from the buffer. Returns
206
213
  # false if more data is needed.
207
214
  def try_to_parse_proxy_protocol
@@ -240,6 +247,7 @@ module Puma
240
247
 
241
248
  return read_body if in_data_phase
242
249
 
250
+ data = nil
243
251
  begin
244
252
  data = @io.read_nonblock(CHUNK_SIZE)
245
253
  rescue IO::WaitReadable
@@ -403,18 +411,33 @@ module Puma
403
411
  return true
404
412
  end
405
413
 
406
- remain = cl.to_i - body.bytesize
414
+ content_length = cl.to_i
415
+
416
+ remain = content_length - body.bytesize
407
417
 
408
418
  if remain <= 0
409
- @body = StringIO.new(body)
410
- @buffer = nil
419
+ # Part of the body is a pipelined request OR garbage. We'll deal with that later.
420
+ if content_length == 0
421
+ @body = EmptyBody
422
+ if body.empty?
423
+ @buffer = nil
424
+ else
425
+ @buffer = body
426
+ end
427
+ elsif remain == 0
428
+ @body = StringIO.new body
429
+ @buffer = nil
430
+ else
431
+ @body = StringIO.new(body[0,content_length])
432
+ @buffer = body[content_length..-1]
433
+ end
411
434
  set_ready
412
435
  return true
413
436
  end
414
437
 
415
438
  if remain > MAX_BODY
416
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
417
- @body.unlink
439
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
440
+ File.unlink @body.path unless IS_WINDOWS
418
441
  @body.binmode
419
442
  @tempfile = @body
420
443
  else
@@ -478,7 +501,7 @@ module Puma
478
501
  def read_chunked_body
479
502
  while true
480
503
  begin
481
- chunk = @io.read_nonblock(4096, @read_buffer)
504
+ chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
482
505
  rescue IO::WaitReadable
483
506
  return false
484
507
  rescue SystemCallError, IOError
@@ -506,8 +529,8 @@ module Puma
506
529
  @prev_chunk = ""
507
530
  @excess_cr = 0
508
531
 
509
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
510
- @body.unlink
532
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
533
+ File.unlink @body.path unless IS_WINDOWS
511
534
  @body.binmode
512
535
  @tempfile = @body
513
536
  @chunked_content_length = 0
@@ -627,7 +650,7 @@ module Puma
627
650
  @partial_part_left = len - part.size
628
651
  end
629
652
  else
630
- if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
653
+ if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
631
654
  raise HttpParserError, "maximum size of chunk header exceeded"
632
655
  end
633
656
 
@@ -92,21 +92,21 @@ module Puma
92
92
  restart_server << true << false
93
93
  else # fork worker
94
94
  worker_pids << pid = spawn_worker(idx)
95
- @worker_write << "f#{pid}:#{idx}\n" rescue nil
95
+ @worker_write << "#{Puma::Const::PipeRequest::FORK}#{pid}:#{idx}\n" rescue nil
96
96
  end
97
97
  end
98
98
  end
99
99
  end
100
100
 
101
101
  Signal.trap "SIGTERM" do
102
- @worker_write << "e#{Process.pid}\n" rescue nil
102
+ @worker_write << "#{Puma::Const::PipeRequest::EXTERNAL_TERM}#{Process.pid}\n" rescue nil
103
103
  restart_server.clear
104
104
  server.stop
105
105
  restart_server << false
106
106
  end
107
107
 
108
108
  begin
109
- @worker_write << "b#{Process.pid}:#{index}\n"
109
+ @worker_write << "#{Puma::Const::PipeRequest::BOOT}#{Process.pid}:#{index}\n"
110
110
  rescue SystemCallError, IOError
111
111
  Puma::Util.purge_interrupt_queue
112
112
  STDERR.puts "Master seems to have exited, exiting."
@@ -131,7 +131,7 @@ module Puma
131
131
  t = server.pool_capacity || 0
132
132
  m = server.max_threads || 0
133
133
  rc = server.requests_count || 0
134
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
134
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads":#{m}, "requests_count":#{rc} }\n!
135
135
  io << payload
136
136
  rescue IOError
137
137
  Puma::Util.purge_interrupt_queue
@@ -147,7 +147,7 @@ module Puma
147
147
  # exiting until any background operations are completed
148
148
  @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
149
149
  ensure
150
- @worker_write << "t#{Process.pid}\n" rescue nil
150
+ @worker_write << "#{Puma::Const::PipeRequest::TERM}#{Process.pid}\n" rescue nil
151
151
  @worker_write.close
152
152
  end
153
153
 
@@ -51,13 +51,12 @@ module Puma
51
51
  @term
52
52
  end
53
53
 
54
+ STATUS_PATTERN = /{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads":(?<max_threads>\d*), "requests_count":(?<requests_count>\d*) }/
55
+ private_constant :STATUS_PATTERN
56
+
54
57
  def ping!(status)
55
58
  @last_checkin = Time.now
56
- captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
57
- @last_status = captures.names.inject({}) do |hash, key|
58
- hash[key.to_sym] = captures[key].to_i
59
- hash
60
- end
59
+ @last_status = status.match(STATUS_PATTERN).named_captures.map { |c_name, c| [c_name.to_sym, c.to_i] }.to_h
61
60
  end
62
61
 
63
62
  # @see Puma::Cluster#check_workers
data/lib/puma/cluster.rb CHANGED
@@ -348,8 +348,6 @@ module Puma
348
348
  def run
349
349
  @status = :run
350
350
 
351
- @idle_workers = {}
352
-
353
351
  output_header "cluster"
354
352
 
355
353
  # This is aligned with the output from Runner, see Runner#output_header
@@ -440,7 +438,7 @@ module Puma
440
438
 
441
439
  while @status == :run
442
440
  begin
443
- if all_workers_idle_timed_out?
441
+ if @options[:idle_timeout] && all_workers_idle_timed_out?
444
442
  log "- All workers reached idle timeout"
445
443
  break
446
444
  end
@@ -456,14 +454,17 @@ module Puma
456
454
 
457
455
  if read.wait_readable([0, @next_check - Time.now].max)
458
456
  req = read.read_nonblock(1)
457
+ next unless req
459
458
 
460
- @next_check = Time.now if req == "!"
461
- next if !req || req == "!"
459
+ if req == Puma::Const::PipeRequest::WAKEUP
460
+ @next_check = Time.now
461
+ next
462
+ end
462
463
 
463
464
  result = read.gets
464
465
  pid = result.to_i
465
466
 
466
- if req == "b" || req == "f"
467
+ if req == Puma::Const::PipeRequest::BOOT || req == Puma::Const::PipeRequest::FORK
467
468
  pid, idx = result.split(':').map(&:to_i)
468
469
  w = worker_at idx
469
470
  w.pid = pid if w.pid.nil?
@@ -471,17 +472,17 @@ module Puma
471
472
 
472
473
  if w = @workers.find { |x| x.pid == pid }
473
474
  case req
474
- when "b"
475
+ when Puma::Const::PipeRequest::BOOT
475
476
  w.boot!
476
477
  log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
477
478
  @next_check = Time.now
478
479
  workers_not_booted -= 1
479
- when "e"
480
+ when Puma::Const::PipeRequest::EXTERNAL_TERM
480
481
  # external term, see worker method, Signal.trap "SIGTERM"
481
482
  w.term!
482
- when "t"
483
+ when Puma::Const::PipeRequest::TERM
483
484
  w.term unless w.term?
484
- when "p"
485
+ when Puma::Const::PipeRequest::PING
485
486
  status = result.sub(/^\d+/,'').chomp
486
487
  w.ping!(status)
487
488
  @events.fire(:ping!, w)
@@ -496,11 +497,11 @@ module Puma
496
497
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
497
498
  booted = true
498
499
  end
499
- when "i"
500
- if @idle_workers[pid]
501
- @idle_workers.delete pid
500
+ when Puma::Const::PipeRequest::IDLE
501
+ if idle_workers[pid]
502
+ idle_workers.delete pid
502
503
  else
503
- @idle_workers[pid] = true
504
+ idle_workers[pid] = true
504
505
  end
505
506
  end
506
507
  else
@@ -560,9 +561,12 @@ module Puma
560
561
  @workers.reject! do |w|
561
562
  next false if w.pid.nil?
562
563
  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))
564
+ # We may need to check the PID individually because:
565
+ # 1. From Ruby versions 2.6 to 3.2, `Process.detach` can prevent or delay
566
+ # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
567
+ # 2. When `fork_worker` is enabled, some worker may not be direct children,
568
+ # but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
569
+ if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
566
570
  true
567
571
  else
568
572
  w.term if w.term?
@@ -602,7 +606,11 @@ module Puma
602
606
  end
603
607
 
604
608
  def idle_timed_out_worker_pids
605
- @idle_workers.keys
609
+ idle_workers.keys
610
+ end
611
+
612
+ def idle_workers
613
+ @idle_workers ||= {}
606
614
  end
607
615
  end
608
616
  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
 
@@ -339,12 +346,22 @@ module Puma
339
346
 
340
347
  private
341
348
 
349
+ def require_processor_counter
350
+ require 'concurrent/utility/processor_counter'
351
+ rescue LoadError
352
+ warn <<~MESSAGE
353
+ WEB_CONCURRENCY=auto requires the "concurrent-ruby" gem to be installed.
354
+ Please add "concurrent-ruby" to your Gemfile.
355
+ MESSAGE
356
+ raise
357
+ end
358
+
342
359
  # Load and use the normal Rack builder if we can, otherwise
343
360
  # fallback to our minimal version.
344
361
  def rack_builder
345
362
  # Load bundler now if we can so that we can pickup rack from
346
363
  # a Gemfile
347
- if ENV.key? 'PUMA_BUNDLER_PRUNED'
364
+ if @puma_bundler_pruned
348
365
  begin
349
366
  require 'bundler/setup'
350
367
  rescue LoadError
@@ -354,11 +371,10 @@ module Puma
354
371
  begin
355
372
  require 'rack'
356
373
  require 'rack/builder'
374
+ ::Rack::Builder
357
375
  rescue LoadError
358
- # ok, use builtin version
359
- return Puma::Rack::Builder
360
- else
361
- return ::Rack::Builder
376
+ require_relative 'rack/builder'
377
+ Puma::Rack::Builder
362
378
  end
363
379
  end
364
380
 
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.5.0"
104
+ CODE_NAME = "Sky's Version"
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,15 @@ 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
+ module PipeRequest
298
+ WAKEUP = "!"
299
+ BOOT = "b"
300
+ FORK = "f"
301
+ EXTERNAL_TERM = "e"
302
+ TERM = "t"
303
+ PING = "p"
304
+ IDLE = "i"
305
+ end
296
306
  end
297
307
  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]