puma 6.6.1 → 7.2.0

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +224 -4
  3. data/README.md +34 -34
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +5 -5
  6. data/docs/jungle/README.md +1 -1
  7. data/docs/kubernetes.md +11 -16
  8. data/docs/plugins.md +2 -2
  9. data/docs/restart.md +2 -2
  10. data/docs/signals.md +19 -19
  11. data/docs/stats.md +4 -3
  12. data/docs/systemd.md +3 -3
  13. data/ext/puma_http11/extconf.rb +2 -17
  14. data/ext/puma_http11/mini_ssl.c +18 -8
  15. data/ext/puma_http11/org/jruby/puma/Http11.java +9 -1
  16. data/ext/puma_http11/puma_http11.c +122 -118
  17. data/lib/puma/app/status.rb +10 -2
  18. data/lib/puma/binder.rb +10 -8
  19. data/lib/puma/cli.rb +3 -5
  20. data/lib/puma/client.rb +52 -56
  21. data/lib/puma/cluster/worker.rb +17 -17
  22. data/lib/puma/cluster/worker_handle.rb +38 -7
  23. data/lib/puma/cluster.rb +23 -23
  24. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  25. data/lib/puma/commonlogger.rb +3 -3
  26. data/lib/puma/configuration.rb +104 -51
  27. data/lib/puma/const.rb +9 -10
  28. data/lib/puma/control_cli.rb +6 -2
  29. data/lib/puma/detect.rb +2 -0
  30. data/lib/puma/dsl.rb +149 -91
  31. data/lib/puma/error_logger.rb +3 -1
  32. data/lib/puma/events.rb +25 -10
  33. data/lib/puma/io_buffer.rb +8 -4
  34. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  35. data/lib/puma/launcher.rb +54 -49
  36. data/lib/puma/minissl.rb +0 -1
  37. data/lib/puma/plugin/systemd.rb +3 -3
  38. data/lib/puma/rack/urlmap.rb +1 -1
  39. data/lib/puma/reactor.rb +19 -13
  40. data/lib/puma/request.rb +42 -31
  41. data/lib/puma/runner.rb +9 -18
  42. data/lib/puma/server.rb +114 -64
  43. data/lib/puma/single.rb +6 -3
  44. data/lib/puma/state_file.rb +3 -2
  45. data/lib/puma/thread_pool.rb +47 -82
  46. data/lib/puma/util.rb +0 -7
  47. data/lib/puma.rb +10 -0
  48. data/lib/rack/handler/puma.rb +2 -2
  49. data/tools/Dockerfile +13 -5
  50. metadata +6 -5
  51. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/launcher.rb CHANGED
@@ -22,12 +22,15 @@ module Puma
22
22
  #
23
23
  # +conf+ A Puma::Configuration object indicating how to run the server.
24
24
  #
25
- # +launcher_args+ A Hash that currently has one required key `:events`,
26
- # this is expected to hold an object similar to an `Puma::LogWriter.stdio`,
27
- # this object will be responsible for broadcasting Puma's internal state
28
- # to a logging destination. An optional key `:argv` can be supplied,
29
- # this should be an array of strings, these arguments are re-used when
30
- # restarting the puma server.
25
+ # +launcher_args+ A Hash that has a few optional keys.
26
+ # - +:log_writer+:: Expected to hold an object similar to `Puma::LogWriter.stdio`.
27
+ # This object will be responsible for broadcasting Puma's internal state
28
+ # to a logging destination.
29
+ # - +:events+:: Expected to hold an object similar to `Puma::Events`.
30
+ # - +:argv+:: Expected to be an array of strings.
31
+ # - +:env+:: Expected to hold a hash of environment variables.
32
+ #
33
+ # These arguments are re-used when restarting the puma server.
31
34
  #
32
35
  # Examples:
33
36
  #
@@ -39,27 +42,44 @@ module Puma
39
42
  # end
40
43
  # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
41
44
  def initialize(conf, launcher_args={})
42
- @runner = nil
43
- @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
44
- @events = launcher_args[:events] || Events.new
45
- @argv = launcher_args[:argv] || []
45
+ ## Minimal initialization before potential early restart (e.g. from bundle pruning)
46
+
47
+ @config = conf
48
+ # Advertise the CLI Configuration before config files are loaded
49
+ Puma.cli_config = @config if defined?(Puma.cli_config)
50
+ @config.clamp
51
+
52
+ @options = @config.options
53
+
54
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
55
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
56
+ @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
57
+ @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
58
+ @options[:log_writer] = @log_writer
59
+ @options[:logger] = @log_writer if clustered?
60
+
61
+ @events = launcher_args[:events] || Events.new
62
+
63
+ @argv = launcher_args[:argv] || []
46
64
  @original_argv = @argv.dup
47
- @config = conf
48
65
 
49
- env = launcher_args.delete(:env) || ENV
66
+ ## End minimal initialization
50
67
 
51
- @config.options[:log_writer] = @log_writer
68
+ generate_restart_data
69
+ Dir.chdir(@restart_dir)
52
70
 
53
- # Advertise the Configuration
54
- Puma.cli_config = @config if defined?(Puma.cli_config)
71
+ prune_bundler!
72
+
73
+ env = launcher_args.delete(:env) || ENV
55
74
 
56
- @config.load
75
+ # Log after prune_bundler! to avoid duplicate logging if a restart occurs
76
+ log_config if env['PUMA_LOG_CONFIG']
57
77
 
58
- @binder = Binder.new(@log_writer, conf)
59
- @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
60
- @binder.create_activated_fds(ENV).each { |k| ENV.delete k }
78
+ @binder = Binder.new(@log_writer, @options)
79
+ @binder.create_inherited_fds(env).each { |k| env.delete k }
80
+ @binder.create_activated_fds(env).each { |k| env.delete k }
61
81
 
62
- @environment = conf.environment
82
+ @environment = @config.environment
63
83
 
64
84
  # Load the systemd integration if we detect systemd's NOTIFY_SOCKET.
65
85
  # Skip this on JRuby though, because it is incompatible with the systemd
@@ -68,37 +88,21 @@ module Puma
68
88
  @config.plugins.create('systemd')
69
89
  end
70
90
 
71
- if @config.options[:bind_to_activated_sockets]
72
- @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
73
- @config.options[:binds],
74
- @config.options[:bind_to_activated_sockets] == 'only'
91
+ if @options[:bind_to_activated_sockets]
92
+ @options[:binds] = @binder.synthesize_binds_from_activated_fs(
93
+ @options[:binds],
94
+ @options[:bind_to_activated_sockets] == 'only'
75
95
  )
76
96
  end
77
97
 
78
- @options = @config.options
79
- @config.clamp
80
-
81
- @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
82
- @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
83
-
84
- @log_writer.custom_logger = options[:custom_logger] if @options[:custom_logger]
85
-
86
- generate_restart_data
87
-
88
98
  if clustered? && !Puma.forkable?
89
99
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
90
100
  end
91
101
 
92
- Dir.chdir(@restart_dir)
93
-
94
- prune_bundler!
95
-
96
102
  @environment = @options[:environment] if @options[:environment]
97
103
  set_rack_environment
98
104
 
99
105
  if clustered?
100
- @options[:logger] = @log_writer
101
-
102
106
  @runner = Cluster.new(self)
103
107
  else
104
108
  @runner = Single.new(self)
@@ -106,8 +110,6 @@ module Puma
106
110
  Puma.stats_object = @runner
107
111
 
108
112
  @status = :run
109
-
110
- log_config if env['PUMA_LOG_CONFIG']
111
113
  end
112
114
 
113
115
  attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
@@ -140,7 +142,10 @@ module Puma
140
142
  # Delete the configured pidfile
141
143
  def delete_pidfile
142
144
  path = @options[:pidfile]
143
- File.unlink(path) if path && File.exist?(path)
145
+ begin
146
+ File.unlink(path) if path
147
+ rescue Errno::ENOENT
148
+ end
144
149
  end
145
150
 
146
151
  # Begin async shutdown of the server
@@ -277,7 +282,7 @@ module Puma
277
282
  end
278
283
 
279
284
  def do_graceful_stop
280
- @events.fire_on_stopped!
285
+ @events.fire_after_stopped!
281
286
  @runner.stop_blocked
282
287
  end
283
288
 
@@ -289,8 +294,8 @@ module Puma
289
294
  end
290
295
 
291
296
  def restart!
292
- @events.fire_on_restart!
293
- @config.run_hooks :on_restart, self, @log_writer
297
+ @events.fire_before_restart!
298
+ @config.run_hooks :before_restart, self, @log_writer
294
299
 
295
300
  if Puma.jruby?
296
301
  close_binder_listeners
@@ -382,9 +387,9 @@ module Puma
382
387
  # using it.
383
388
  @restart_dir = Dir.pwd
384
389
 
385
- # Use the same trick as unicorn, namely favor PWD because
386
- # it will contain an unresolved symlink, useful for when
387
- # the pwd is /data/releases/current.
390
+ # Use the same trick as unicorn, namely favor PWD because
391
+ # it will contain an unresolved symlink, useful for when
392
+ # the pwd is /data/releases/current.
388
393
  elsif dir = ENV['PWD']
389
394
  s_env = File.stat(dir)
390
395
  s_pwd = File.stat(Dir.pwd)
data/lib/puma/minissl.rb CHANGED
@@ -172,7 +172,6 @@ module Puma
172
172
  end
173
173
  end
174
174
  rescue IOError, SystemCallError
175
- Puma::Util.purge_interrupt_queue
176
175
  # nothing
177
176
  ensure
178
177
  @socket.close
@@ -14,9 +14,9 @@ Puma::Plugin.create do
14
14
  launcher.log_writer.log "* Enabling systemd notification integration"
15
15
 
16
16
  # hook_events
17
- launcher.events.on_booted { Puma::SdNotify.ready }
18
- launcher.events.on_stopped { Puma::SdNotify.stopping }
19
- launcher.events.on_restart { Puma::SdNotify.reloading }
17
+ launcher.events.after_booted { Puma::SdNotify.ready }
18
+ launcher.events.after_stopped { Puma::SdNotify.stopping }
19
+ launcher.events.before_restart { Puma::SdNotify.reloading }
20
20
 
21
21
  # start watchdog
22
22
  if Puma::SdNotify.watchdog?
@@ -70,7 +70,7 @@ module Puma::Rack
70
70
  return app.call(env)
71
71
  end
72
72
 
73
- [404, {'Content-Type' => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
73
+ [404, {'content-type' => "text/plain", "x-cascade" => "pass"}, ["Not Found: #{path}"]]
74
74
 
75
75
  ensure
76
76
  env['PATH_INFO'] = path
data/lib/puma/reactor.rb CHANGED
@@ -15,6 +15,12 @@ module Puma
15
15
  #
16
16
  # The implementation uses a Queue to synchronize adding new objects from the internal select loop.
17
17
  class Reactor
18
+
19
+ # @!attribute [rw] reactor_max
20
+ # Maximum number of clients in the selector. Reset with calls to `Server.stats`.
21
+ attr_accessor :reactor_max
22
+ attr_reader :reactor_size
23
+
18
24
  # Create a new Reactor to monitor IO objects added by #add.
19
25
  # The provided block will be invoked when an IO has data available to read,
20
26
  # its timeout elapses, or when the Reactor shuts down.
@@ -29,6 +35,8 @@ module Puma
29
35
  @input = Queue.new
30
36
  @timeouts = []
31
37
  @block = block
38
+ @reactor_size = 0
39
+ @reactor_max = 0
32
40
  end
33
41
 
34
42
  # Run the internal select loop, using a background thread by default.
@@ -67,17 +75,18 @@ module Puma
67
75
  private
68
76
 
69
77
  def select_loop
70
- close_selector = true
71
78
  begin
72
79
  until @input.closed? && @input.empty?
73
80
  # Wakeup any registered object that receives incoming data.
74
81
  # Block until the earliest timeout or Selector#wakeup is called.
75
82
  timeout = (earliest = @timeouts.first) && earliest.timeout
76
- @selector.select(timeout) {|mon| wakeup!(mon.value)}
83
+ @selector.select(timeout) do |monitor|
84
+ wakeup!(monitor.value)
85
+ end
77
86
 
78
87
  # Wakeup all objects that timed out.
79
- timed_out = @timeouts.take_while {|t| t.timeout == 0}
80
- timed_out.each { |c| wakeup! c }
88
+ timed_out = @timeouts.take_while { |client| client.timeout == 0 }
89
+ timed_out.each { |client| wakeup!(client) }
81
90
 
82
91
  unless @input.empty?
83
92
  until @input.empty?
@@ -91,23 +100,19 @@ module Puma
91
100
  STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})"
92
101
  STDERR.puts e.backtrace
93
102
 
94
- # NoMethodError may be rarely raised when calling @selector.select, which
95
- # is odd. Regardless, it may continue for thousands of calls if retried.
96
- # Also, when it raises, @selector.close also raises an error.
97
- if NoMethodError === e
98
- close_selector = false
99
- else
100
- retry
101
- end
103
+ retry
102
104
  end
105
+
103
106
  # Wakeup all remaining objects on shutdown.
104
107
  @timeouts.each(&@block)
105
- @selector.close if close_selector
108
+ @selector.close
106
109
  end
107
110
 
108
111
  # Start monitoring the object.
109
112
  def register(client)
110
113
  @selector.register(client.to_io, :r).value = client
114
+ @reactor_size += 1
115
+ @reactor_max = @reactor_size if @reactor_max < @reactor_size
111
116
  @timeouts << client
112
117
  rescue ArgumentError
113
118
  # unreadable clients raise error when processed by NIO
@@ -118,6 +123,7 @@ module Puma
118
123
  def wakeup!(client)
119
124
  if @block.call client
120
125
  @selector.deregister client.to_io
126
+ @reactor_size -= 1
121
127
  @timeouts.delete client
122
128
  end
123
129
  end
data/lib/puma/request.rb CHANGED
@@ -36,24 +36,27 @@ module Puma
36
36
  # Takes the request contained in +client+, invokes the Rack application to construct
37
37
  # the response and writes it back to +client.io+.
38
38
  #
39
- # It'll return +false+ when the connection is closed, this doesn't mean
39
+ # It'll return +:close+ when the connection is closed, this doesn't mean
40
40
  # that the response wasn't successful.
41
41
  #
42
+ # It'll return +:keep_alive+ if the connection is a pipeline or keep-alive connection.
43
+ # Which may contain additional requests.
44
+ #
42
45
  # It'll return +:async+ if the connection remains open but will be handled
43
46
  # elsewhere, i.e. the connection has been hijacked by the Rack application.
44
47
  #
45
48
  # Finally, it'll return +true+ on keep-alive connections.
46
49
  # @param client [Puma::Client]
47
50
  # @param requests [Integer]
48
- # @return [Boolean,:async]
49
- #
51
+ # @return [:close, :keep_alive, :async]
50
52
  def handle_request(client, requests)
51
53
  env = client.env
52
54
  io_buffer = client.io_buffer
53
55
  socket = client.io # io may be a MiniSSL::Socket
54
56
  app_body = nil
57
+ error = nil
55
58
 
56
- return false if closed_socket?(socket)
59
+ return :close if closed_socket?(socket)
57
60
 
58
61
  if client.http_content_length_limit_exceeded
59
62
  return prepare_response(413, {}, ["Payload Too Large"], requests, client)
@@ -68,7 +71,7 @@ module Puma
68
71
  end
69
72
 
70
73
  env[HIJACK_P] = true
71
- env[HIJACK] = client
74
+ env[HIJACK] = client.method :full_hijack
72
75
 
73
76
  env[RACK_INPUT] = client.body
74
77
  env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
@@ -92,6 +95,7 @@ module Puma
92
95
  # array, we will invoke them when the request is done.
93
96
  #
94
97
  env[RACK_AFTER_REPLY] ||= []
98
+ env[RACK_RESPONSE_FINISHED] ||= []
95
99
 
96
100
  begin
97
101
  if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
@@ -119,15 +123,15 @@ module Puma
119
123
 
120
124
  return :async
121
125
  end
122
- rescue ThreadPool::ForceShutdown => e
123
- @log_writer.unknown_error e, client, "Rack app"
126
+ rescue ThreadPool::ForceShutdown => error
127
+ @log_writer.unknown_error error, client, "Rack app"
124
128
  @log_writer.log "Detected force shutdown of a thread"
125
129
 
126
- status, headers, res_body = lowlevel_error(e, env, 503)
127
- rescue Exception => e
128
- @log_writer.unknown_error e, client, "Rack app"
130
+ status, headers, res_body = lowlevel_error(error, env, 503)
131
+ rescue Exception => error
132
+ @log_writer.unknown_error error, client, "Rack app"
129
133
 
130
- status, headers, res_body = lowlevel_error(e, env, 500)
134
+ status, headers, res_body = lowlevel_error(error, env, 500)
131
135
  end
132
136
  prepare_response(status, headers, res_body, requests, client)
133
137
  ensure
@@ -144,6 +148,16 @@ module Puma
144
148
  end
145
149
  end
146
150
  end
151
+
152
+ if response_finished = env[RACK_RESPONSE_FINISHED]
153
+ response_finished.reverse_each do |o|
154
+ begin
155
+ o.call(env, status, headers, error)
156
+ rescue StandardError => e
157
+ @log_writer.debug_error e
158
+ end
159
+ end
160
+ end
147
161
  end
148
162
 
149
163
  # Assembles the headers and prepares the body for actually sending the
@@ -155,26 +169,16 @@ module Puma
155
169
  # a call to `Server#lowlevel_error`
156
170
  # @param requests [Integer] number of inline requests handled
157
171
  # @param client [Puma::Client]
158
- # @return [Boolean,:async] keep-alive status or `:async`
172
+ # @return [:close, :keep_alive, :async]
159
173
  def prepare_response(status, headers, res_body, requests, client)
160
174
  env = client.env
161
175
  socket = client.io
162
176
  io_buffer = client.io_buffer
163
177
 
164
- return false if closed_socket?(socket)
178
+ return :close if closed_socket?(socket)
165
179
 
166
180
  # Close the connection after a reasonable number of inline requests
167
- # if the server is at capacity and the listener has a new connection ready.
168
- # This allows Puma to service connections fairly when the number
169
- # of concurrent connections exceeds the size of the threadpool.
170
- force_keep_alive = if @enable_keep_alives
171
- requests < @max_fast_inline ||
172
- @thread_pool.busy_threads < @max_threads ||
173
- !client.listener.to_io.wait_readable(0)
174
- else
175
- # Always set force_keep_alive to false if the server has keep-alives not enabled.
176
- false
177
- end
181
+ force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
178
182
 
179
183
  resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
180
184
 
@@ -242,7 +246,7 @@ module Puma
242
246
  io_buffer << LINE_END
243
247
  fast_write_str socket, io_buffer.read_and_reset
244
248
  socket.flush
245
- return keep_alive
249
+ return keep_alive ? :keep_alive : :close
246
250
  end
247
251
  else
248
252
  if content_length
@@ -267,14 +271,21 @@ module Puma
267
271
 
268
272
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
269
273
  body.close if close_body
270
- keep_alive
274
+ # if we're shutting down, close keep_alive connections
275
+ !shutting_down? && keep_alive ? :keep_alive : :close
271
276
  end
272
277
 
278
+ HTTP_ON_VALUES = { "on" => true, HTTPS => true }
279
+ private_constant :HTTP_ON_VALUES
280
+
273
281
  # @param env [Hash] see Puma::Client#env, from request
274
282
  # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
275
283
  #
276
284
  def default_server_port(env)
277
- if ['on', HTTPS].include?(env[HTTPS_KEY]) || env[HTTP_X_FORWARDED_PROTO].to_s[0...5] == HTTPS || env[HTTP_X_FORWARDED_SCHEME] == HTTPS || env[HTTP_X_FORWARDED_SSL] == "on"
285
+ if HTTP_ON_VALUES[env[HTTPS_KEY]] ||
286
+ env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
287
+ env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
288
+ env[HTTP_X_FORWARDED_SSL] == "on"
278
289
  PORT_443
279
290
  else
280
291
  PORT_80
@@ -478,7 +489,7 @@ module Puma
478
489
 
479
490
  # The legacy HTTP_VERSION header can be sent as a client header.
480
491
  # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
481
- env[HTTP_VERSION] = env[SERVER_PROTOCOL]
492
+ env[HTTP_VERSION] = env[SERVER_PROTOCOL] if @env_set_http_version
482
493
  end
483
494
  private :normalize_env
484
495
 
@@ -585,7 +596,7 @@ module Puma
585
596
  # response body
586
597
  # @param io_buffer [Puma::IOBuffer] modified inn place
587
598
  # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
588
- # status and `@max_fast_inline`
599
+ # status and `@max_keep_alive`
589
600
  # @return [Hash] resp_info
590
601
  # @version 5.0.3
591
602
  #
@@ -668,10 +679,10 @@ module Puma
668
679
  if ary
669
680
  ary.each do |v|
670
681
  next if illegal_header_value?(v)
671
- io_buffer.append k, colon, v, line_ending
682
+ io_buffer.append k.downcase, colon, v, line_ending
672
683
  end
673
684
  else
674
- io_buffer.append k, colon, line_ending
685
+ io_buffer.append k.downcase, colon, line_ending
675
686
  end
676
687
  end
677
688
 
data/lib/puma/runner.rb CHANGED
@@ -33,7 +33,6 @@ module Puma
33
33
  @wakeup.write PIPE_WAKEUP unless @wakeup.closed?
34
34
 
35
35
  rescue SystemCallError, IOError
36
- Puma::Util.purge_interrupt_queue
37
36
  end
38
37
 
39
38
  def development?
@@ -71,7 +70,7 @@ module Puma
71
70
  token = nil if token.empty? || token == 'none'
72
71
  end
73
72
 
74
- app = Puma::App::Status.new @launcher, token
73
+ app = Puma::App::Status.new @launcher, token: token, data_only: @options[:control_data_only]
75
74
 
76
75
  # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
77
76
  # Use `nil` for events, no hooks in control server
@@ -93,22 +92,6 @@ module Puma
93
92
  @control.binder.close_listeners if @control
94
93
  end
95
94
 
96
- # @!attribute [r] ruby_engine
97
- # @deprecated Use `RUBY_DESCRIPTION` instead
98
- def ruby_engine
99
- warn "Puma::Runner#ruby_engine is deprecated; use RUBY_DESCRIPTION instead. It will be removed in puma v7."
100
-
101
- if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
102
- "ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
103
- else
104
- if defined?(RUBY_ENGINE_VERSION)
105
- "#{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} - ruby #{RUBY_VERSION}"
106
- else
107
- "#{RUBY_ENGINE} #{RUBY_VERSION}"
108
- end
109
- end
110
- end
111
-
112
95
  def output_header(mode)
113
96
  min_t = @options[:min_threads]
114
97
  max_t = @options[:max_threads]
@@ -128,6 +111,14 @@ module Puma
128
111
  end
129
112
  end
130
113
 
114
+ def warn_ruby_mn_threads
115
+ return if !ENV.key?('RUBY_MN_THREADS')
116
+
117
+ log "! WARNING: Detected `RUBY_MN_THREADS=#{ENV['RUBY_MN_THREADS']}`"
118
+ log "! This setting is known to cause performance regressions with Puma."
119
+ log "! Consider disabling this environment variable: https://github.com/puma/puma/issues/3720"
120
+ end
121
+
131
122
  def redirected_io?
132
123
  @options[:redirect_stdout] || @options[:redirect_stderr]
133
124
  end