puma 7.2.0 → 8.0.2

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.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'socket'
4
+ require 'uri'
5
+
3
6
  require_relative 'plugin'
4
7
  require_relative 'const'
5
8
  require_relative 'dsl'
@@ -131,14 +134,16 @@ module Puma
131
134
 
132
135
  DEFAULTS = {
133
136
  auto_trim_time: 30,
134
- binds: ['tcp://0.0.0.0:9292'.freeze],
135
- fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
137
+ binds: ['tcp://[::]:9292'.freeze],
136
138
  debug: false,
137
- enable_keep_alives: true,
138
139
  early_hints: nil,
140
+ enable_keep_alives: true,
139
141
  environment: 'development'.freeze,
142
+ fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
140
143
  # Number of seconds to wait until we get the first data for the request.
141
144
  first_data_timeout: 30,
145
+ force_shutdown_after: -1,
146
+ http_content_length_limit: nil,
142
147
  # Number of seconds to wait until the next request before shutting down.
143
148
  idle_timeout: nil,
144
149
  io_selector_backend: :auto,
@@ -147,6 +152,7 @@ module Puma
147
152
  # Limits how many requests a keep alive connection can make.
148
153
  # The connection will be closed after it reaches `max_keep_alive`
149
154
  # requests.
155
+ max_io_threads: 0,
150
156
  max_keep_alive: 999,
151
157
  max_threads: Puma.mri? ? 5 : 16,
152
158
  min_threads: 0,
@@ -161,10 +167,10 @@ module Puma
161
167
  raise_exception_on_sigterm: true,
162
168
  reaping_time: 1,
163
169
  remote_address: :socket,
164
- silence_single_worker_warning: false,
165
170
  silence_fork_callback_warning: false,
171
+ silence_single_worker_warning: false,
166
172
  tag: File.basename(Dir.getwd),
167
- tcp_host: '0.0.0.0'.freeze,
173
+ tcp_host: '::'.freeze,
168
174
  tcp_port: 9292,
169
175
  wait_for_less_busy_worker: 0.005,
170
176
  worker_boot_timeout: 60,
@@ -173,11 +179,10 @@ module Puma
173
179
  worker_shutdown_timeout: 30,
174
180
  worker_timeout: 60,
175
181
  workers: 0,
176
- http_content_length_limit: nil
177
182
  }
178
183
 
179
184
  def initialize(user_options={}, default_options = {}, env = ENV, &block)
180
- default_options = self.puma_default_options(env).merge(default_options)
185
+ default_options = self.puma_default_options(env).merge(events: Events.new).merge(default_options)
181
186
 
182
187
  @_options = UserFileDefaultOptions.new(user_options, default_options)
183
188
  @plugins = PluginLoader.new
@@ -230,6 +235,8 @@ module Puma
230
235
 
231
236
  def puma_default_options(env = ENV)
232
237
  defaults = DEFAULTS.dup
238
+ defaults[:tcp_host] = self.class.default_tcp_host
239
+ defaults[:binds] = [self.class.default_tcp_bind]
233
240
  puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
234
241
  defaults
235
242
  end
@@ -277,8 +284,10 @@ module Puma
277
284
  # This also calls load if it hasn't been called yet.
278
285
  def clamp
279
286
  load unless @loaded
287
+ run_mode_hooks
280
288
  set_conditional_default_options
281
289
  @_options.finalize_values
290
+ rewrite_unavailable_ipv6_binds!
282
291
  @clamped = true
283
292
  warn_hooks
284
293
  options
@@ -335,7 +344,7 @@ module Puma
335
344
  # @param arg [Launcher, Int] `:before_restart` passes Launcher
336
345
  #
337
346
  def run_hooks(key, arg, log_writer, hook_data = nil)
338
- log_writer.debug "Running #{key} hooks"
347
+ log_writer.debug { "Running #{key} hooks" }
339
348
 
340
349
  options.all_of(key).each do |hook_options|
341
350
  begin
@@ -357,6 +366,23 @@ module Puma
357
366
  options.final_options
358
367
  end
359
368
 
369
+ def self.default_tcp_host
370
+ ipv6_interface_available? ? Const::UNSPECIFIED_IPV6 : Const::UNSPECIFIED_IPV4
371
+ end
372
+
373
+ def self.default_tcp_bind(port = DEFAULTS[:tcp_port])
374
+ URI::Generic.build(scheme: 'tcp', host: default_tcp_host, port: Integer(port)).to_s
375
+ end
376
+
377
+ def self.ipv6_interface_available?
378
+ Socket.getifaddrs.any? do |ifaddr|
379
+ addr = ifaddr.addr
380
+ addr&.ipv6? && !addr&.ipv6_loopback?
381
+ end
382
+ rescue StandardError
383
+ false
384
+ end
385
+
360
386
  def self.temp_path
361
387
  require 'tmpdir'
362
388
 
@@ -372,6 +398,31 @@ module Puma
372
398
 
373
399
  private
374
400
 
401
+ def rewrite_unavailable_ipv6_binds!
402
+ return if self.class.ipv6_interface_available?
403
+
404
+ tried_ipv6_bind = false
405
+ bind_schemes = ['tcp', 'ssl']
406
+
407
+ @_options[:binds] = Array(@_options[:binds]).map do |bind|
408
+ uri = URI.parse(bind)
409
+ next bind unless bind_schemes.include?(uri.scheme)
410
+
411
+ host = uri.host&.delete_prefix('[')&.delete_suffix(']')
412
+ next bind unless host&.include?(':')
413
+
414
+ tried_ipv6_bind = true
415
+ next bind unless host == Const::UNSPECIFIED_IPV6
416
+
417
+ uri.host = Const::UNSPECIFIED_IPV4
418
+ uri.to_s
419
+ rescue URI::InvalidURIError
420
+ bind
421
+ end
422
+
423
+ warn "WARNING: IPv6 bind requested but no non-loopback IPv6 interface was detected" if tried_ipv6_bind
424
+ end
425
+
375
426
  def require_processor_counter
376
427
  require 'concurrent/utility/processor_counter'
377
428
  rescue LoadError
@@ -433,6 +484,17 @@ module Puma
433
484
  rack_app
434
485
  end
435
486
 
487
+ def run_mode_hooks
488
+ workers_before = @_options[:workers]
489
+ key = workers_before > 0 ? :cluster : :single
490
+
491
+ @_options.all_of(key).each(&:call)
492
+
493
+ unless @_options[:workers] == workers_before
494
+ raise "cannot change the number of workers inside a #{key} configuration hook"
495
+ end
496
+ end
497
+
436
498
  def set_conditional_default_options
437
499
  @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
438
500
  (@_options[:workers] > 1) && Puma.forkable?
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 = "7.2.0"
104
- CODE_NAME = "On The Corner"
103
+ PUMA_VERSION = VERSION = "8.0.2"
104
+ CODE_NAME = "Into the Arena"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
@@ -291,7 +291,8 @@ module Puma
291
291
  # Banned keys of response header
292
292
  BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
293
293
 
294
- PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
294
+ PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
295
+ PROXY_PROTOCOL_V1_MAX_LENGTH = 107
295
296
 
296
297
  # All constants are prefixed with `PIPE_` to avoid name collisions.
297
298
  module PipeRequest
@@ -16,7 +16,7 @@ module Puma
16
16
  'gc' => nil,
17
17
  'gc-stats' => nil,
18
18
  'halt' => 'SIGQUIT',
19
- 'info' => 'SIGINFO',
19
+ 'info' => Puma.backtrace_signal,
20
20
  'phased-restart' => 'SIGUSR1',
21
21
  'refork' => 'SIGURG',
22
22
  'reload-worker-directory' => nil,
data/lib/puma/detect.rb CHANGED
@@ -35,6 +35,13 @@ module Puma
35
35
  IS_WINDOWS
36
36
  end
37
37
 
38
+ BACKTRACE_SIGNAL =
39
+ if Signal.list.key?("INFO")
40
+ "SIGINFO"
41
+ elsif Signal.list.key?("PWR")
42
+ "SIGPWR"
43
+ end
44
+
38
45
  # @version 5.0.0
39
46
  def self.mri?
40
47
  IS_MRI
@@ -44,4 +51,8 @@ module Puma
44
51
  def self.forkable?
45
52
  HAS_FORK
46
53
  end
54
+
55
+ def self.backtrace_signal
56
+ BACKTRACE_SIGNAL
57
+ end
47
58
  end
data/lib/puma/dsl.rb CHANGED
@@ -155,7 +155,7 @@ module Puma
155
155
  end
156
156
 
157
157
  def default_host
158
- @options[:default_host] || Configuration::DEFAULTS[:tcp_host]
158
+ @options[:default_host] || Configuration.default_tcp_host
159
159
  end
160
160
 
161
161
  def inject(&blk)
@@ -260,7 +260,8 @@ module Puma
260
260
  # accepted protocols. Multiple urls can be bound to, calling +bind+ does
261
261
  # not overwrite previous bindings.
262
262
  #
263
- # The default is "tcp://0.0.0.0:9292".
263
+ # The default is "tcp://[::]:9292" when IPv6 interfaces are available,
264
+ # otherwise "tcp://0.0.0.0:9292".
264
265
  #
265
266
  # You can use query parameters within the url to specify options:
266
267
  #
@@ -279,7 +280,7 @@ module Puma
279
280
  # @example SSL cert for mutual TLS (mTLS)
280
281
  # bind 'ssl://127.0.0.1:9292?key=key.key&cert=cert.pem&ca=ca.pem&verify_mode=force_peer'
281
282
  # @example Disable optimization for low latency
282
- # bind 'tcp://0.0.0.0:9292?low_latency=false'
283
+ # bind 'tcp://[::]:9292?low_latency=false'
283
284
  # @example Socket permissions
284
285
  # bind 'unix:///var/run/puma.sock?umask=0111'
285
286
  #
@@ -349,7 +350,7 @@ module Puma
349
350
 
350
351
  # Define how long persistent connections can be idle before Puma closes them.
351
352
  #
352
- # The default is 20 seconds.
353
+ # The default is 65 seconds.
353
354
  #
354
355
  # @example
355
356
  # persistent_timeout 30
@@ -595,6 +596,29 @@ module Puma
595
596
  @options[:max_threads] = max
596
597
  end
597
598
 
599
+ # Configure the max number of IO threads.
600
+ #
601
+ # When request handlers know the current requests will no longer use a significant amount
602
+ # of CPU, they can mark the current request as IO bound using <tt>env["puma.mark_as_io_bound"]</tt>.
603
+ #
604
+ # Threads marked as IO bound are allowed to go over the max thread limit.
605
+ #
606
+ # @example
607
+ # threads 5
608
+ # max_io_threads 5
609
+ #
610
+ # The above example allows for 5 regular threads and 5 IO threads to process requests concurrently.
611
+ # Any IO thread over the limit is counted as a regular thread, hence the above configuration also
612
+ # allows for 3 regular threads and 7 IO threads for example.
613
+ def max_io_threads(max)
614
+ max = Integer(max)
615
+ if max < 0
616
+ raise "The maximum number of IO threads (#{max}) must be a positive number"
617
+ end
618
+
619
+ @options[:max_io_threads] = max
620
+ end
621
+
598
622
  # Instead of using +bind+ and manually constructing a URI like:
599
623
  #
600
624
  # bind 'ssl://127.0.0.1:9292?key=key_path&cert=cert_path'
@@ -727,6 +751,44 @@ module Puma
727
751
  @options[:silence_fork_callback_warning] = true
728
752
  end
729
753
 
754
+ # Code to run only in single mode.
755
+ # Runs after all config files are loaded.
756
+ #
757
+ # This can be called multiple times.
758
+ #
759
+ # @note Single mode only.
760
+ #
761
+ # @example
762
+ # single do
763
+ # silence_fork_callback_warning
764
+ # end
765
+ #
766
+ def single(&block)
767
+ raise ArgumentError, "A block must be provided to `single`" unless block
768
+
769
+ @options[:single] ||= []
770
+ @options[:single] << block
771
+ end
772
+
773
+ # Code to run only in cluster mode.
774
+ # Runs after all config files are loaded.
775
+ #
776
+ # This can be called multiple times.
777
+ #
778
+ # @note Cluster mode only.
779
+ #
780
+ # @example
781
+ # cluster do
782
+ # prune_bundler
783
+ # end
784
+ #
785
+ def cluster(&block)
786
+ raise ArgumentError, "A block must be provided to `cluster`" unless block
787
+
788
+ @options[:cluster] ||= []
789
+ @options[:cluster] << block
790
+ end
791
+
730
792
  # Code to run immediately before master process
731
793
  # forks workers (once on boot). These hooks can block if necessary
732
794
  # to wait for background operations unknown to Puma to finish before
@@ -1040,6 +1102,7 @@ module Puma
1040
1102
  # new Bundler context and thus can float around as the release
1041
1103
  # dictates.
1042
1104
  #
1105
+ # @note Cluster mode only.
1043
1106
  # @note This is incompatible with +preload_app!+.
1044
1107
  # @note This is only supported for RubyGems 2.2+
1045
1108
  #
@@ -1145,7 +1208,7 @@ module Puma
1145
1208
 
1146
1209
  # Change the default worker timeout for booting.
1147
1210
  #
1148
- # The default is the value of `worker_timeout`.
1211
+ # The default is 60 seconds.
1149
1212
  #
1150
1213
  # @note Cluster mode only.
1151
1214
  #
@@ -1227,11 +1290,14 @@ module Puma
1227
1290
  # threads will be written to $stdout. This can help figure
1228
1291
  # out why shutdown is hanging.
1229
1292
  #
1230
- def shutdown_debug(val=true)
1231
- @options[:shutdown_debug] = val
1293
+ # If `on_force` is true, the backtraces will be written only
1294
+ # when the shutdown is forced i.e. not graceful.
1295
+ #
1296
+ # @see force_shutdown_after
1297
+ def shutdown_debug(val = true, on_force: false)
1298
+ @options[:shutdown_debug] = val && on_force ? :on_force : val
1232
1299
  end
1233
1300
 
1234
-
1235
1301
  # Maximum delay of worker accept loop.
1236
1302
  #
1237
1303
  # Attempts to route traffic to less-busy workers by causing a busy worker to delay
@@ -28,14 +28,12 @@ module Puma
28
28
 
29
29
  log '* Pruning Bundler environment'
30
30
  home = ENV['GEM_HOME']
31
- bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
32
- bundle_app_config = Bundler.original_env['BUNDLE_APP_CONFIG']
31
+ original_bundle_env = Bundler.original_env.select { |k, v| k.start_with?('BUNDLE_') && v }
33
32
 
34
33
  with_unbundled_env do
35
34
  ENV['GEM_HOME'] = home
36
- ENV['BUNDLE_GEMFILE'] = bundle_gemfile
37
35
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
38
- ENV["BUNDLE_APP_CONFIG"] = bundle_app_config
36
+ original_bundle_env.each { |k, v| ENV[k] = v }
39
37
  args = [Gem.ruby, puma_wild_path, '-I', dirs.join(':')] + @original_argv
40
38
  # Defaults to true which breaks socket activation
41
39
  args += [{:close_others => false}]
data/lib/puma/launcher.rb CHANGED
@@ -476,8 +476,8 @@ module Puma
476
476
  end
477
477
 
478
478
  begin
479
- unless Puma.jruby? # INFO in use by JVM already
480
- Signal.trap "SIGINFO" do
479
+ if Puma.backtrace_signal
480
+ Signal.trap Puma.backtrace_signal do
481
481
  thread_status do |name, backtrace|
482
482
  @log_writer.log(name)
483
483
  @log_writer.log(backtrace.map { |bt| " #{bt}" })
@@ -485,8 +485,7 @@ module Puma
485
485
  end
486
486
  end
487
487
  rescue Exception
488
- # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
489
- # to see this constantly on Linux.
488
+ log "*** SIGINFO/SIGPWR not implemented, signal based backtrace unavailable!"
490
489
  end
491
490
  end
492
491
 
@@ -90,8 +90,14 @@ module Puma
90
90
  @debug
91
91
  end
92
92
 
93
- def debug(str)
94
- log("% #{str}") if @debug
93
+ def debug(str=nil, &block)
94
+ if @debug
95
+ if block_given?
96
+ log("% #{yield}")
97
+ else
98
+ log("% #{str}")
99
+ end
100
+ end
95
101
  end
96
102
 
97
103
  # Write +str+ to +@stderr+