puma 6.4.2-java → 6.5.0-java

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/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.2"
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.
@@ -281,9 +281,27 @@ module Puma
281
281
  # header values can contain HTAB?
282
282
  ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
283
283
 
284
+ # The keys of headers that should not be convert to underscore
285
+ # normalized versions. These headers are ignored at the request reading layer,
286
+ # but if we normalize them after reading, it's just confusing for the application.
287
+ UNMASKABLE_HEADERS = {
288
+ "HTTP_TRANSFER,ENCODING" => true,
289
+ "HTTP_CONTENT,LENGTH" => true,
290
+ }
291
+
284
292
  # Banned keys of response header
285
293
  BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
286
294
 
287
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
288
306
  end
289
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]