puma 7.2.2 → 8.0.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.
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+
5
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
8
+ # This module is included in `Client`. It contains code to process the `env`
9
+ # before it is passed to the app.
10
+ #
11
+ module ClientEnv # :nodoc:
12
+
13
+ include Puma::Const
14
+
15
+ # Given a Hash +env+ for the request read from +client+, add
16
+ # and fixup keys to comply with Rack's env guidelines.
17
+ # @param env [Hash] see Puma::Client#env, from request
18
+ # @param client [Puma::Client] only needed for Client#peerip
19
+ #
20
+ def normalize_env
21
+ if host = @env[HTTP_HOST]
22
+ # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
23
+ if colon = host.rindex("]:") # IPV6 with port
24
+ @env[SERVER_NAME] = host[0, colon+1]
25
+ @env[SERVER_PORT] = host[colon+2, host.bytesize]
26
+ elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
27
+ @env[SERVER_NAME] = host[0, colon]
28
+ @env[SERVER_PORT] = host[colon+1, host.bytesize]
29
+ else
30
+ @env[SERVER_NAME] = host
31
+ @env[SERVER_PORT] = default_server_port
32
+ end
33
+ else
34
+ @env[SERVER_NAME] = LOCALHOST
35
+ @env[SERVER_PORT] = default_server_port
36
+ end
37
+
38
+ unless @env[REQUEST_PATH]
39
+ # it might be a dumbass full host request header
40
+ uri = begin
41
+ URI.parse(@env[REQUEST_URI])
42
+ rescue URI::InvalidURIError
43
+ raise Puma::HttpParserError
44
+ end
45
+ @env[REQUEST_PATH] = uri.path
46
+
47
+ # A nil env value will cause a LintError (and fatal errors elsewhere),
48
+ # so only set the env value if there actually is a value.
49
+ @env[QUERY_STRING] = uri.query if uri.query
50
+ end
51
+
52
+ @env[PATH_INFO] = @env[REQUEST_PATH].to_s # #to_s in case it's nil
53
+
54
+ # From https://www.ietf.org/rfc/rfc3875 :
55
+ # "Script authors should be aware that the REMOTE_ADDR and
56
+ # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
57
+ # may not identify the ultimate source of the request.
58
+ # They identify the client for the immediate request to the
59
+ # server; that client may be a proxy, gateway, or other
60
+ # intermediary acting on behalf of the actual source client."
61
+ #
62
+
63
+ unless @env.key?(REMOTE_ADDR)
64
+ begin
65
+ addr = peerip
66
+ rescue Errno::ENOTCONN
67
+ # Client disconnects can result in an inability to get the
68
+ # peeraddr from the socket; default to unspec.
69
+ if peer_family == Socket::AF_INET6
70
+ addr = UNSPECIFIED_IPV6
71
+ else
72
+ addr = UNSPECIFIED_IPV4
73
+ end
74
+ end
75
+
76
+ # Set unix socket addrs to localhost
77
+ if addr.empty?
78
+ addr = peer_family == Socket::AF_INET6 ? LOCALHOST_IPV6 : LOCALHOST_IPV4
79
+ end
80
+
81
+ @env[REMOTE_ADDR] = addr
82
+ end
83
+
84
+ # The legacy HTTP_VERSION header can be sent as a client header.
85
+ # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
86
+ @env[HTTP_VERSION] = @env[SERVER_PROTOCOL] if @env_set_http_version
87
+
88
+ @env[PUMA_SOCKET] = @io
89
+
90
+ if @env[HTTPS_KEY] && @io.peercert
91
+ @env[PUMA_PEERCERT] = @io.peercert
92
+ end
93
+
94
+ @env[HIJACK_P] = true
95
+ @env[HIJACK] = method(:full_hijack).to_proc
96
+
97
+ @env[RACK_INPUT] = @body || EmptyBody
98
+ @env[RACK_URL_SCHEME] ||= default_server_port == PORT_443 ? HTTPS : HTTP
99
+ end
100
+
101
+ # Fixup any headers with `,` in the name to have `_` now. We emit
102
+ # headers with `,` in them during the parse phase to avoid ambiguity
103
+ # with the `-` to `_` conversion for critical headers. But here for
104
+ # compatibility, we'll convert them back. This code is written to
105
+ # avoid allocation in the common case (ie there are no headers
106
+ # with `,` in their names), that's why it has the extra conditionals.
107
+ #
108
+ # @note If a normalized version of a `,` header already exists, we ignore
109
+ # the `,` version. This prevents clobbering headers managed by proxies
110
+ # but not by clients (Like X-Forwarded-For).
111
+ #
112
+ # @param env [Hash] see Puma::Client#env, from request, modifies in place
113
+ # @version 5.0.3
114
+ #
115
+ def req_env_post_parse
116
+ to_delete = nil
117
+ to_add = nil
118
+
119
+ @env.each do |k,v|
120
+ if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
121
+ if to_delete
122
+ to_delete << k
123
+ else
124
+ to_delete = [k]
125
+ end
126
+
127
+ new_k = k.tr(",", "_")
128
+ if @env.key?(new_k)
129
+ next
130
+ end
131
+
132
+ unless to_add
133
+ to_add = {}
134
+ end
135
+
136
+ to_add[new_k] = v
137
+ end
138
+ end
139
+
140
+ if to_delete # rubocop:disable Style/SafeNavigation
141
+ to_delete.each { |k| env.delete(k) }
142
+ end
143
+
144
+ if to_add
145
+ @env.merge! to_add
146
+ end
147
+
148
+ # A rack extension. If the app writes #call'ables to this
149
+ # array, we will invoke them when the request is done.
150
+ #
151
+ env[RACK_AFTER_REPLY] ||= []
152
+ env[RACK_RESPONSE_FINISHED] ||= []
153
+ end
154
+
155
+ HTTP_ON_VALUES = { "on" => true, HTTPS => true }
156
+ private_constant :HTTP_ON_VALUES
157
+
158
+ # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
159
+ #
160
+ def default_server_port
161
+ if HTTP_ON_VALUES[@env[HTTPS_KEY]] ||
162
+ @env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
163
+ @env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
164
+ @env[HTTP_X_FORWARDED_SSL] == "on"
165
+ PORT_443
166
+ else
167
+ PORT_80
168
+ end
169
+ end
170
+ end
171
+ end
data/lib/puma/cluster.rb CHANGED
@@ -87,7 +87,7 @@ module Puma
87
87
  end
88
88
 
89
89
  debug "Spawned worker: #{pid}"
90
- @workers << WorkerHandle.new(idx, pid, @phase, @options)
90
+ @workers.insert(idx, WorkerHandle.new(idx, pid, @phase, @options))
91
91
  end
92
92
 
93
93
  if @options[:fork_worker] && all_workers_in_phase?
@@ -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,15 +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,
142
- shutdown_body_timeout: 25,
145
+ force_shutdown_after: -1,
146
+ http_content_length_limit: nil,
143
147
  # Number of seconds to wait until the next request before shutting down.
144
148
  idle_timeout: nil,
145
149
  io_selector_backend: :auto,
@@ -148,6 +152,7 @@ module Puma
148
152
  # Limits how many requests a keep alive connection can make.
149
153
  # The connection will be closed after it reaches `max_keep_alive`
150
154
  # requests.
155
+ max_io_threads: 0,
151
156
  max_keep_alive: 999,
152
157
  max_threads: Puma.mri? ? 5 : 16,
153
158
  min_threads: 0,
@@ -162,10 +167,10 @@ module Puma
162
167
  raise_exception_on_sigterm: true,
163
168
  reaping_time: 1,
164
169
  remote_address: :socket,
165
- silence_single_worker_warning: false,
166
170
  silence_fork_callback_warning: false,
171
+ silence_single_worker_warning: false,
167
172
  tag: File.basename(Dir.getwd),
168
- tcp_host: '0.0.0.0'.freeze,
173
+ tcp_host: '::'.freeze,
169
174
  tcp_port: 9292,
170
175
  wait_for_less_busy_worker: 0.005,
171
176
  worker_boot_timeout: 60,
@@ -174,11 +179,10 @@ module Puma
174
179
  worker_shutdown_timeout: 30,
175
180
  worker_timeout: 60,
176
181
  workers: 0,
177
- http_content_length_limit: nil
178
182
  }
179
183
 
180
184
  def initialize(user_options={}, default_options = {}, env = ENV, &block)
181
- 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)
182
186
 
183
187
  @_options = UserFileDefaultOptions.new(user_options, default_options)
184
188
  @plugins = PluginLoader.new
@@ -231,6 +235,8 @@ module Puma
231
235
 
232
236
  def puma_default_options(env = ENV)
233
237
  defaults = DEFAULTS.dup
238
+ defaults[:tcp_host] = self.class.default_tcp_host
239
+ defaults[:binds] = [self.class.default_tcp_bind]
234
240
  puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
235
241
  defaults
236
242
  end
@@ -278,8 +284,10 @@ module Puma
278
284
  # This also calls load if it hasn't been called yet.
279
285
  def clamp
280
286
  load unless @loaded
287
+ run_mode_hooks
281
288
  set_conditional_default_options
282
289
  @_options.finalize_values
290
+ rewrite_unavailable_ipv6_binds!
283
291
  @clamped = true
284
292
  warn_hooks
285
293
  options
@@ -358,6 +366,23 @@ module Puma
358
366
  options.final_options
359
367
  end
360
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
+
361
386
  def self.temp_path
362
387
  require 'tmpdir'
363
388
 
@@ -373,6 +398,31 @@ module Puma
373
398
 
374
399
  private
375
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
+
376
426
  def require_processor_counter
377
427
  require 'concurrent/utility/processor_counter'
378
428
  rescue LoadError
@@ -434,6 +484,17 @@ module Puma
434
484
  rack_app
435
485
  end
436
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
+
437
498
  def set_conditional_default_options
438
499
  @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
439
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.2"
104
- CODE_NAME = "On The Corner"
103
+ PUMA_VERSION = VERSION = "8.0.0"
104
+ CODE_NAME = "Into the Arena"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
@@ -109,9 +109,6 @@ module Puma
109
109
  # sending data back
110
110
  WRITE_TIMEOUT = 10
111
111
 
112
- # How long the accept loop waits after running out of file descriptors
113
- ACCEPT_ERROR_DELAY = 0.1
114
-
115
112
  # The original URI requested by the client.
116
113
  REQUEST_URI= "REQUEST_URI"
117
114
  REQUEST_PATH = "REQUEST_PATH"
@@ -294,8 +291,7 @@ module Puma
294
291
  # Banned keys of response header
295
292
  BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
296
293
 
297
- PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
298
- PROXY_PROTOCOL_V1_MAX_LENGTH = 107
294
+ PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
299
295
 
300
296
  # All constants are prefixed with `PIPE_` to avoid name collisions.
301
297
  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,
@@ -44,7 +44,6 @@ module Puma
44
44
  @pid = nil
45
45
  @control_url = nil
46
46
  @control_auth_token = nil
47
- @control_ca_file = nil
48
47
  @config_file = nil
49
48
  @command = nil
50
49
  @environment = env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV']
@@ -81,10 +80,6 @@ module Puma
81
80
  @control_auth_token = arg
82
81
  end
83
82
 
84
- o.on "--control-ca-file PATH", "CA certificate bundle for the TLS control server" do |arg|
85
- @control_ca_file = arg
86
- end
87
-
88
83
  o.on "-F", "--config-file PATH", "Puma config script" do |arg|
89
84
  @config_file = arg
90
85
  end
@@ -184,16 +179,11 @@ module Puma
184
179
  when 'ssl'
185
180
  require 'openssl'
186
181
  host = host[1..-2] if host&.start_with? '['
187
- ctx = OpenSSL::SSL::SSLContext.new
188
- ctx.ca_file = @control_ca_file if @control_ca_file
189
- ctx.set_params
190
- ssl = OpenSSL::SSL::SSLSocket.new(
182
+ OpenSSL::SSL::SSLSocket.new(
191
183
  TCPSocket.new(host, uri.port),
192
- ctx)
193
- ssl.tap { |socket| socket.sync_close = true }
194
- .tap { |socket| socket.hostname = host }
184
+ OpenSSL::SSL::SSLContext.new)
185
+ .tap { |ssl| ssl.sync_close = true } # default is false
195
186
  .tap(&:connect)
196
- .tap { |socket| socket.post_connection_check host }
197
187
  when 'tcp'
198
188
  host = host[1..-2] if host&.start_with? '['
199
189
  TCPSocket.new host, uri.port
@@ -240,7 +230,6 @@ module Puma
240
230
  message response.last if PRINTABLE_COMMANDS.include?(@command)
241
231
  end
242
232
  ensure
243
- server ||= ssl
244
233
  if server
245
234
  if uri.scheme == 'ssl'
246
235
  server.sysclose
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
@@ -398,20 +399,7 @@ module Puma
398
399
  @options[:drain_on_shutdown] = which
399
400
  end
400
401
 
401
- # Define how long an incomplete request may continue to be read during shutdown.
402
- #
403
- # The default is 25 seconds.
404
- #
405
- # @example
406
- # shutdown_body_timeout 10
407
- #
408
- # @see Puma::Server#graceful_shutdown
409
- #
410
- def shutdown_body_timeout(seconds)
411
- @options[:shutdown_body_timeout] = Float(seconds)
412
- end
413
-
414
- # Set the environment in which the Rack app will run. The value must be
402
+ # Set the environment in which the rack's app will run. The value must be
415
403
  # a string.
416
404
  #
417
405
  # The default is "development".
@@ -608,6 +596,29 @@ module Puma
608
596
  @options[:max_threads] = max
609
597
  end
610
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
+
611
622
  # Instead of using +bind+ and manually constructing a URI like:
612
623
  #
613
624
  # bind 'ssl://127.0.0.1:9292?key=key_path&cert=cert_path'
@@ -740,6 +751,44 @@ module Puma
740
751
  @options[:silence_fork_callback_warning] = true
741
752
  end
742
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
+
743
792
  # Code to run immediately before master process
744
793
  # forks workers (once on boot). These hooks can block if necessary
745
794
  # to wait for background operations unknown to Puma to finish before
@@ -1053,6 +1102,7 @@ module Puma
1053
1102
  # new Bundler context and thus can float around as the release
1054
1103
  # dictates.
1055
1104
  #
1105
+ # @note Cluster mode only.
1056
1106
  # @note This is incompatible with +preload_app!+.
1057
1107
  # @note This is only supported for RubyGems 2.2+
1058
1108
  #
@@ -1158,7 +1208,7 @@ module Puma
1158
1208
 
1159
1209
  # Change the default worker timeout for booting.
1160
1210
  #
1161
- # The default is the value of `worker_timeout`.
1211
+ # The default is 60 seconds.
1162
1212
  #
1163
1213
  # @note Cluster mode only.
1164
1214
  #
@@ -1240,11 +1290,14 @@ module Puma
1240
1290
  # threads will be written to $stdout. This can help figure
1241
1291
  # out why shutdown is hanging.
1242
1292
  #
1243
- def shutdown_debug(val=true)
1244
- @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
1245
1299
  end
1246
1300
 
1247
-
1248
1301
  # Maximum delay of worker accept loop.
1249
1302
  #
1250
1303
  # Attempts to route traffic to less-busy workers by causing a busy worker to delay
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