puma 7.0.4 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +139 -0
  3. data/README.md +25 -13
  4. data/docs/5.0-Upgrade.md +98 -0
  5. data/docs/6.0-Upgrade.md +56 -0
  6. data/docs/7.0-Upgrade.md +52 -0
  7. data/docs/8.0-Upgrade.md +100 -0
  8. data/docs/deployment.md +58 -23
  9. data/docs/grpc.md +62 -0
  10. data/docs/images/favicon.svg +1 -0
  11. data/docs/images/running-puma.svg +1 -0
  12. data/docs/images/standard-logo.svg +1 -0
  13. data/docs/jungle/README.md +1 -1
  14. data/docs/kubernetes.md +5 -12
  15. data/docs/plugins.md +2 -2
  16. data/docs/signals.md +10 -10
  17. data/docs/stats.md +2 -2
  18. data/docs/systemd.md +3 -3
  19. data/ext/puma_http11/http11_parser.java.rl +51 -65
  20. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  21. data/ext/puma_http11/org/jruby/puma/Http11.java +168 -104
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  23. data/ext/puma_http11/puma_http11.c +101 -109
  24. data/lib/puma/app/status.rb +10 -2
  25. data/lib/puma/cli.rb +1 -1
  26. data/lib/puma/client.rb +111 -91
  27. data/lib/puma/client_env.rb +171 -0
  28. data/lib/puma/cluster/worker.rb +10 -9
  29. data/lib/puma/cluster/worker_handle.rb +2 -2
  30. data/lib/puma/cluster.rb +12 -11
  31. data/lib/puma/cluster_accept_loop_delay.rb +17 -18
  32. data/lib/puma/configuration.rb +86 -16
  33. data/lib/puma/const.rb +2 -2
  34. data/lib/puma/control_cli.rb +1 -1
  35. data/lib/puma/detect.rb +11 -0
  36. data/lib/puma/dsl.rb +115 -18
  37. data/lib/puma/launcher.rb +35 -30
  38. data/lib/puma/reactor.rb +3 -12
  39. data/lib/puma/{request.rb → response.rb} +25 -194
  40. data/lib/puma/runner.rb +1 -1
  41. data/lib/puma/server.rb +102 -63
  42. data/lib/puma/server_plugin_control.rb +32 -0
  43. data/lib/puma/single.rb +2 -2
  44. data/lib/puma/state_file.rb +3 -2
  45. data/lib/puma/thread_pool.rb +139 -24
  46. data/lib/rack/handler/puma.rb +1 -1
  47. data/tools/Dockerfile +13 -5
  48. metadata +16 -5
  49. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/cluster.rb CHANGED
@@ -23,7 +23,7 @@ module Puma
23
23
  @next_check = Time.now
24
24
 
25
25
  @worker_max = [] # keeps track of 'max' stat values
26
- @phased_restart = false
26
+ @pending_phased_restart = false
27
27
  end
28
28
 
29
29
  # Returns the list of cluster worker handles.
@@ -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?
@@ -186,7 +186,7 @@ module Puma
186
186
  # we need to phase any workers out (which will restart
187
187
  # in the right phase).
188
188
  #
189
- w = @workers.find { |x| x.phase != @phase }
189
+ w = @workers.find { |x| x.phase < @phase }
190
190
 
191
191
  if w
192
192
  if refork
@@ -221,12 +221,11 @@ module Puma
221
221
  pipes[:wakeup] = @wakeup
222
222
  end
223
223
 
224
- server = start_server if preload?
225
224
  new_worker = Worker.new index: index,
226
225
  master: master,
227
226
  launcher: @launcher,
228
227
  pipes: pipes,
229
- server: server
228
+ app: (app if preload?)
230
229
  new_worker.run
231
230
  end
232
231
 
@@ -238,7 +237,7 @@ module Puma
238
237
  def phased_restart(refork = false)
239
238
  return false if @options[:preload_app] && !refork
240
239
 
241
- @phased_restart = refork ? :refork : true
240
+ @pending_phased_restart = refork ? :refork : true
242
241
  wakeup!
243
242
 
244
243
  true
@@ -456,11 +455,11 @@ module Puma
456
455
  break
457
456
  end
458
457
 
459
- if @phased_restart
460
- start_phased_restart(@phased_restart == :refork)
458
+ if @pending_phased_restart
459
+ start_phased_restart(@pending_phased_restart == :refork)
461
460
 
462
- in_phased_restart = @phased_restart
463
- @phased_restart = false
461
+ in_phased_restart = @pending_phased_restart
462
+ @pending_phased_restart = false
464
463
 
465
464
  workers_not_booted = @options[:workers]
466
465
  # worker 0 is not restarted on refork
@@ -583,7 +582,9 @@ module Puma
583
582
  # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
584
583
  # 2. When `fork_worker` is enabled, some worker may not be direct children,
585
584
  # but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
586
- if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
585
+ if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
586
+ w.process_status = status
587
+ @config.run_hooks(:after_worker_shutdown, w, @log_writer)
587
588
  true
588
589
  else
589
590
  w.term if w.term?
@@ -20,48 +20,47 @@ module Puma
20
20
  # already https://github.com/puma/puma/pull/3678/files/2736ebddb3fc8528e5150b5913fba251c37a8bf7#diff-a95f46e7ce116caddc9b9a9aa81004246d5210d5da5f4df90a818c780630166bL251-L291
21
21
  #
22
22
  # With the introduction of true keepalive support, there are two ways a request can come in:
23
- # - A new request from a new client comes into the socket and it must be "accept"-d
23
+ # - A new request from a new client comes into the socket and it must be "accept"-ed
24
24
  # - A keepalive request is served and the connection is retained. Another request is then accepted
25
25
  #
26
26
  # Ideally the server handles requests in the order they come in, and ideally it doesn't accept more requests than it can handle.
27
27
  # These goals are contradictory, because when the server is at maximum capacity due to keepalive connections, it could mean we
28
28
  # block all new requests, even if those came in before the new request on the older keepalive connection.
29
29
  #
30
- # ## Distribute CPU resources across all workers
30
+ # ## Goal: Distribute CPU resources across all workers
31
31
  #
32
32
  # - This issue was opened https://github.com/puma/puma/issues/2078
33
33
  #
34
- # There are several entangled issues and it's not exactly clear the root cause, but the observable outcome
34
+ # There are several entangled issues and it's not exactly clear what the root cause is, but the observable outcome
35
35
  # was that performance was better with a small sleep, and that eventually became the default.
36
36
  #
37
37
  # An attempt to describe why this works is here: https://github.com/puma/puma/issues/2078#issuecomment-3287032470.
38
38
  #
39
39
  # Summarizing: The delay is for tuning the rate at which "accept" is called on the socket.
40
- # Puma works by calling "accept" nonblock on the socket in a loop. When there are multiple workers,
41
- # (processes) then they will "race" to accept a request at roughly the same rate. However if one
40
+ # Puma works by calling "accept" nonblock on the socket in a loop. When there are multiple workers
41
+ # (processes), they will "race" to accept a request at roughly the same rate. However, if one
42
42
  # worker has all threads busy processing requests, then accepting a new request might "steal" it from
43
43
  # a less busy worker. If a worker has no work to do, it should loop as fast as possible.
44
44
  #
45
- # ## Solution(s): Distribute requests across workers at start
45
+ # ## Solution: Distribute requests across workers at start
46
46
  #
47
47
  # For now, both goals are framed as "load balancing" across workers (processes) and achieved through
48
48
  # the same mechanism of sleeping longer to delay busier workers. Rather than the prior Puma 6.x
49
- # and earlier behavior of using a binary on/off sleep value, we increase it an amound proportional
50
- # to the load the server is under. Capping the maximum delay to the scenario where all threads are busy
49
+ # and earlier behavior of using a binary on/off sleep value, we increase it an amount proportional
50
+ # to the load the server is under, capping the maximum delay to the scenario where all threads are busy
51
51
  # and the todo list has reached a multiplier of the maximum number of threads.
52
52
  #
53
53
  # Private: API may change unexpectedly
54
54
  class ClusterAcceptLoopDelay
55
- attr_reader :max_threads, :max_delay
55
+ attr_reader :max_delay
56
56
 
57
- # Initialize happens once, `call` happens often. Push global calculations here
57
+ # Initialize happens once, `call` happens often. Perform global calculations here.
58
58
  def initialize(
59
- # Number of workers in the cluster
60
- workers: ,
61
- # Maximum delay in seconds i.e. 0.005 is 5 microseconds
62
- max_delay: # In seconds i.e. 0.005 is 5 microseconds
63
-
64
- )
59
+ # Number of workers in the cluster
60
+ workers: ,
61
+ # Maximum delay in seconds i.e. 0.005 is 5 milliseconds
62
+ max_delay:
63
+ )
65
64
  @on = max_delay > 0 && workers >= 2
66
65
  @max_delay = max_delay.to_f
67
66
 
@@ -76,12 +75,12 @@ module Puma
76
75
  # We want the extreme values of this delay to be known (minimum and maximum) as well as
77
76
  # a predictable curve between the two. i.e. no step functions or hard cliffs.
78
77
  #
79
- # Return value is always numeric. Returns 0 if there should be no delay
78
+ # Return value is always numeric. Returns 0 if there should be no delay.
80
79
  def calculate(
81
80
  # Number of threads working right now, plus number of requests in the todo list
82
81
  busy_threads_plus_todo:,
83
82
  # Maximum number of threads in the pool, note that the busy threads (alone) may go over this value at times
84
- # if the pool needs to be reaped. The busy thread plus todo count may go over this value by a large amount
83
+ # if the pool needs to be reaped. The busy thread plus todo count may go over this value by a large amount.
85
84
  max_threads:
86
85
  )
87
86
  max_value = @overload_multiplier * max_threads
@@ -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,
@@ -155,15 +161,16 @@ module Puma
155
161
  out_of_band: [],
156
162
  # Number of seconds for another request within a persistent session.
157
163
  persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
164
+ prune_bundler: false,
158
165
  queue_requests: true,
159
166
  rackup: 'config.ru'.freeze,
160
167
  raise_exception_on_sigterm: true,
161
168
  reaping_time: 1,
162
169
  remote_address: :socket,
163
- silence_single_worker_warning: false,
164
170
  silence_fork_callback_warning: false,
171
+ silence_single_worker_warning: false,
165
172
  tag: File.basename(Dir.getwd),
166
- tcp_host: '0.0.0.0'.freeze,
173
+ tcp_host: '::'.freeze,
167
174
  tcp_port: 9292,
168
175
  wait_for_less_busy_worker: 0.005,
169
176
  worker_boot_timeout: 60,
@@ -172,11 +179,10 @@ module Puma
172
179
  worker_shutdown_timeout: 30,
173
180
  worker_timeout: 60,
174
181
  workers: 0,
175
- http_content_length_limit: nil
176
182
  }
177
183
 
178
184
  def initialize(user_options={}, default_options = {}, env = ENV, &block)
179
- 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)
180
186
 
181
187
  @_options = UserFileDefaultOptions.new(user_options, default_options)
182
188
  @plugins = PluginLoader.new
@@ -196,7 +202,7 @@ module Puma
196
202
  @clamped = false
197
203
  end
198
204
 
199
- attr_reader :plugins, :events, :hooks
205
+ attr_reader :plugins, :events, :hooks, :_options
200
206
 
201
207
  def options
202
208
  raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
@@ -229,6 +235,8 @@ module Puma
229
235
 
230
236
  def puma_default_options(env = ENV)
231
237
  defaults = DEFAULTS.dup
238
+ defaults[:tcp_host] = self.class.default_tcp_host
239
+ defaults[:binds] = [self.class.default_tcp_bind]
232
240
  puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
233
241
  defaults
234
242
  end
@@ -237,18 +245,14 @@ module Puma
237
245
  min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
238
246
  max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
239
247
  persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
240
- workers = if env['WEB_CONCURRENCY'] == 'auto'
241
- require_processor_counter
242
- ::Concurrent.available_processor_count
243
- else
244
- env['WEB_CONCURRENCY']
245
- end
248
+ workers_env = env['WEB_CONCURRENCY']
249
+ workers = workers_env && workers_env.strip != "" ? parse_workers(workers_env.strip) : nil
246
250
 
247
251
  {
248
252
  min_threads: min && min != "" && Integer(min),
249
253
  max_threads: max && max != "" && Integer(max),
250
254
  persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
251
- workers: workers && workers != "" && Integer(workers),
255
+ workers: workers,
252
256
  environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
253
257
  }
254
258
  end
@@ -280,8 +284,10 @@ module Puma
280
284
  # This also calls load if it hasn't been called yet.
281
285
  def clamp
282
286
  load unless @loaded
287
+ run_mode_hooks
283
288
  set_conditional_default_options
284
289
  @_options.finalize_values
290
+ rewrite_unavailable_ipv6_binds!
285
291
  @clamped = true
286
292
  warn_hooks
287
293
  options
@@ -360,6 +366,23 @@ module Puma
360
366
  options.final_options
361
367
  end
362
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
+
363
386
  def self.temp_path
364
387
  require 'tmpdir'
365
388
 
@@ -375,16 +398,52 @@ module Puma
375
398
 
376
399
  private
377
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
+
378
426
  def require_processor_counter
379
427
  require 'concurrent/utility/processor_counter'
380
428
  rescue LoadError
381
429
  warn <<~MESSAGE
382
- WEB_CONCURRENCY=auto requires the "concurrent-ruby" gem to be installed.
430
+ WEB_CONCURRENCY=auto or workers(:auto) requires the "concurrent-ruby" gem to be installed.
383
431
  Please add "concurrent-ruby" to your Gemfile.
384
432
  MESSAGE
385
433
  raise
386
434
  end
387
435
 
436
+ def parse_workers(value)
437
+ if value == :auto || value == 'auto'
438
+ require_processor_counter
439
+ Integer(::Concurrent.available_processor_count)
440
+ else
441
+ Integer(value)
442
+ end
443
+ rescue ArgumentError, TypeError
444
+ raise ArgumentError, "workers must be an Integer or :auto"
445
+ end
446
+
388
447
  # Load and use the normal Rack builder if we can, otherwise
389
448
  # fallback to our minimal version.
390
449
  def rack_builder
@@ -425,6 +484,17 @@ module Puma
425
484
  rack_app
426
485
  end
427
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
+
428
498
  def set_conditional_default_options
429
499
  @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
430
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.0.4"
104
- CODE_NAME = "Romantic Warrior"
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
 
@@ -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)
@@ -216,6 +216,8 @@ module Puma
216
216
  # activate_control_app 'unix:///var/run/pumactl.sock', { auth_token: '12345' }
217
217
  # @example
218
218
  # activate_control_app 'unix:///var/run/pumactl.sock', { no_token: true }
219
+ # @example
220
+ # activate_control_app 'unix:///var/run/pumactl.sock', { no_token: true, data_only: true}
219
221
  #
220
222
  def activate_control_app(url="auto", opts={})
221
223
  if url == "auto"
@@ -240,6 +242,7 @@ module Puma
240
242
 
241
243
  @options[:control_auth_token] = auth_token
242
244
  @options[:control_url_umask] = opts[:umask] if opts[:umask]
245
+ @options[:control_data_only] = opts[:data_only] if opts[:data_only]
243
246
  end
244
247
 
245
248
  # Load additional configuration from a file.
@@ -257,7 +260,8 @@ module Puma
257
260
  # accepted protocols. Multiple urls can be bound to, calling +bind+ does
258
261
  # not overwrite previous bindings.
259
262
  #
260
- # 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".
261
265
  #
262
266
  # You can use query parameters within the url to specify options:
263
267
  #
@@ -276,7 +280,7 @@ module Puma
276
280
  # @example SSL cert for mutual TLS (mTLS)
277
281
  # bind 'ssl://127.0.0.1:9292?key=key.key&cert=cert.pem&ca=ca.pem&verify_mode=force_peer'
278
282
  # @example Disable optimization for low latency
279
- # bind 'tcp://0.0.0.0:9292?low_latency=false'
283
+ # bind 'tcp://[::]:9292?low_latency=false'
280
284
  # @example Socket permissions
281
285
  # bind 'unix:///var/run/puma.sock?umask=0111'
282
286
  #
@@ -346,7 +350,7 @@ module Puma
346
350
 
347
351
  # Define how long persistent connections can be idle before Puma closes them.
348
352
  #
349
- # The default is 20 seconds.
353
+ # The default is 65 seconds.
350
354
  #
351
355
  # @example
352
356
  # persistent_timeout 30
@@ -592,6 +596,29 @@ module Puma
592
596
  @options[:max_threads] = max
593
597
  end
594
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
+
595
622
  # Instead of using +bind+ and manually constructing a URI like:
596
623
  #
597
624
  # bind 'ssl://127.0.0.1:9292?key=key_path&cert=cert_path'
@@ -656,7 +683,8 @@ module Puma
656
683
  @options[:state] = path.to_s
657
684
  end
658
685
 
659
- # Use +permission+ to restrict permissions for the state file.
686
+ # Use +permission+ to restrict permissions for the state file. By convention,
687
+ # +permission+ is an octal number (e.g. `0640` or `0o640`).
660
688
  #
661
689
  # @example
662
690
  # state_permission 0600
@@ -665,21 +693,27 @@ module Puma
665
693
  @options[:state_permission] = permission
666
694
  end
667
695
 
668
- # How many worker processes to run. Typically this is set to
669
- # the number of available cores.
696
+ # How many worker processes to run. Typically this is set to the number of
697
+ # available cores.
670
698
  #
671
699
  # The default is the value of the environment variable +WEB_CONCURRENCY+ if
672
- # set, otherwise 0.
700
+ # set, otherwise 0. Passing +:auto+ will set the value to
701
+ # +Concurrent.available_processor_count+ (requires the concurrent-ruby gem).
702
+ # On some platforms (e.g. under CPU quotas) this may be fractional, and Puma
703
+ # will round down. If it rounds down to 0, Puma will run in single mode and
704
+ # cluster-only hooks like +before_worker_boot+ will not execute.
705
+ # If you rely on cluster-only hooks, set an explicit worker count.
673
706
  #
674
- # @note Cluster mode only.
707
+ # A value of 0 or nil means run in single mode.
675
708
  #
676
709
  # @example
677
710
  # workers 2
711
+ # workers :auto
678
712
  #
679
713
  # @see Puma::Cluster
680
714
  #
681
715
  def workers(count)
682
- @options[:workers] = count.to_i
716
+ @options[:workers] = count.nil? ? 0 : @config.send(:parse_workers, count)
683
717
  end
684
718
 
685
719
  # Disable warning message when running in cluster mode with a single worker.
@@ -717,6 +751,44 @@ module Puma
717
751
  @options[:silence_fork_callback_warning] = true
718
752
  end
719
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
+
720
792
  # Code to run immediately before master process
721
793
  # forks workers (once on boot). These hooks can block if necessary
722
794
  # to wait for background operations unknown to Puma to finish before
@@ -818,6 +890,20 @@ module Puma
818
890
 
819
891
  alias_method :after_worker_boot, :after_worker_fork
820
892
 
893
+ # Code to run in the master right after a worker has stopped. The worker's
894
+ # index and Process::Status are passed as arguments.
895
+ #
896
+ # @note Cluster mode only.
897
+ #
898
+ # @example
899
+ # after_worker_shutdown do |worker_handle|
900
+ # puts 'Worker crashed' unless worker_handle.process_status.success?
901
+ # end
902
+ #
903
+ def after_worker_shutdown(&block)
904
+ process_hook :after_worker_shutdown, nil, block, cluster_only: true
905
+ end
906
+
821
907
  # Code to run after puma is booted (works for both single and cluster modes).
822
908
  #
823
909
  # @example
@@ -980,6 +1066,7 @@ module Puma
980
1066
  # The default is +true+ if your app uses more than 1 worker.
981
1067
  #
982
1068
  # @note Cluster mode only.
1069
+ # @note When using `fork_worker`, this only applies to worker 0.
983
1070
  #
984
1071
  # @example
985
1072
  # preload_app!
@@ -1015,6 +1102,7 @@ module Puma
1015
1102
  # new Bundler context and thus can float around as the release
1016
1103
  # dictates.
1017
1104
  #
1105
+ # @note Cluster mode only.
1018
1106
  # @note This is incompatible with +preload_app!+.
1019
1107
  # @note This is only supported for RubyGems 2.2+
1020
1108
  #
@@ -1120,7 +1208,7 @@ module Puma
1120
1208
 
1121
1209
  # Change the default worker timeout for booting.
1122
1210
  #
1123
- # The default is the value of `worker_timeout`.
1211
+ # The default is 60 seconds.
1124
1212
  #
1125
1213
  # @note Cluster mode only.
1126
1214
  #
@@ -1202,18 +1290,27 @@ module Puma
1202
1290
  # threads will be written to $stdout. This can help figure
1203
1291
  # out why shutdown is hanging.
1204
1292
  #
1205
- def shutdown_debug(val=true)
1206
- @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
1207
1299
  end
1208
1300
 
1209
-
1210
- # Attempts to route traffic to less-busy workers by causing them to delay
1211
- # listening on the socket, allowing workers which are not processing any
1301
+ # Maximum delay of worker accept loop.
1302
+ #
1303
+ # Attempts to route traffic to less-busy workers by causing a busy worker to delay
1304
+ # listening on the socket, allowing workers which are not processing as many
1212
1305
  # requests to pick up new requests first.
1213
1306
  #
1214
1307
  # The default is 0.005 seconds.
1215
1308
  #
1216
- # Only works on MRI. For all other interpreters, this setting does nothing.
1309
+ # To turn off this feature, set the value to 0.
1310
+ #
1311
+ # @note Cluster mode with >= 2 workers only.
1312
+ #
1313
+ # @note Interpreters with forking support only.
1217
1314
  #
1218
1315
  # @see Puma::Server#handle_servers
1219
1316
  # @see Puma::ThreadPool#wait_for_less_busy_worker
@@ -1357,7 +1454,7 @@ module Puma
1357
1454
  #
1358
1455
  # The default is +:auto+.
1359
1456
  #
1360
- # @see https://github.com/socketry/nio4r/blob/master/lib/nio/selector.rb
1457
+ # @see https://github.com/socketry/nio4r/blob/main/lib/nio/selector.rb
1361
1458
  #
1362
1459
  def io_selector_backend(backend)
1363
1460
  @options[:io_selector_backend] = backend.to_sym