puma 7.0.4 → 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.
@@ -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
@@ -155,6 +155,7 @@ module Puma
155
155
  out_of_band: [],
156
156
  # Number of seconds for another request within a persistent session.
157
157
  persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
158
+ prune_bundler: false,
158
159
  queue_requests: true,
159
160
  rackup: 'config.ru'.freeze,
160
161
  raise_exception_on_sigterm: true,
@@ -196,7 +197,7 @@ module Puma
196
197
  @clamped = false
197
198
  end
198
199
 
199
- attr_reader :plugins, :events, :hooks
200
+ attr_reader :plugins, :events, :hooks, :_options
200
201
 
201
202
  def options
202
203
  raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
@@ -237,18 +238,14 @@ module Puma
237
238
  min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
238
239
  max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
239
240
  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
241
+ workers_env = env['WEB_CONCURRENCY']
242
+ workers = workers_env && workers_env.strip != "" ? parse_workers(workers_env.strip) : nil
246
243
 
247
244
  {
248
245
  min_threads: min && min != "" && Integer(min),
249
246
  max_threads: max && max != "" && Integer(max),
250
247
  persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
251
- workers: workers && workers != "" && Integer(workers),
248
+ workers: workers,
252
249
  environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
253
250
  }
254
251
  end
@@ -379,12 +376,23 @@ module Puma
379
376
  require 'concurrent/utility/processor_counter'
380
377
  rescue LoadError
381
378
  warn <<~MESSAGE
382
- WEB_CONCURRENCY=auto requires the "concurrent-ruby" gem to be installed.
379
+ WEB_CONCURRENCY=auto or workers(:auto) requires the "concurrent-ruby" gem to be installed.
383
380
  Please add "concurrent-ruby" to your Gemfile.
384
381
  MESSAGE
385
382
  raise
386
383
  end
387
384
 
385
+ def parse_workers(value)
386
+ if value == :auto || value == 'auto'
387
+ require_processor_counter
388
+ Integer(::Concurrent.available_processor_count)
389
+ else
390
+ Integer(value)
391
+ end
392
+ rescue ArgumentError, TypeError
393
+ raise ArgumentError, "workers must be an Integer or :auto"
394
+ end
395
+
388
396
  # Load and use the normal Rack builder if we can, otherwise
389
397
  # fallback to our minimal version.
390
398
  def rack_builder
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 = "7.2.0"
104
+ CODE_NAME = "On The Corner"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
data/lib/puma/dsl.rb CHANGED
@@ -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.
@@ -656,7 +659,8 @@ module Puma
656
659
  @options[:state] = path.to_s
657
660
  end
658
661
 
659
- # Use +permission+ to restrict permissions for the state file.
662
+ # Use +permission+ to restrict permissions for the state file. By convention,
663
+ # +permission+ is an octal number (e.g. `0640` or `0o640`).
660
664
  #
661
665
  # @example
662
666
  # state_permission 0600
@@ -665,21 +669,27 @@ module Puma
665
669
  @options[:state_permission] = permission
666
670
  end
667
671
 
668
- # How many worker processes to run. Typically this is set to
669
- # the number of available cores.
672
+ # How many worker processes to run. Typically this is set to the number of
673
+ # available cores.
670
674
  #
671
675
  # The default is the value of the environment variable +WEB_CONCURRENCY+ if
672
- # set, otherwise 0.
676
+ # set, otherwise 0. Passing +:auto+ will set the value to
677
+ # +Concurrent.available_processor_count+ (requires the concurrent-ruby gem).
678
+ # On some platforms (e.g. under CPU quotas) this may be fractional, and Puma
679
+ # will round down. If it rounds down to 0, Puma will run in single mode and
680
+ # cluster-only hooks like +before_worker_boot+ will not execute.
681
+ # If you rely on cluster-only hooks, set an explicit worker count.
673
682
  #
674
- # @note Cluster mode only.
683
+ # A value of 0 or nil means run in single mode.
675
684
  #
676
685
  # @example
677
686
  # workers 2
687
+ # workers :auto
678
688
  #
679
689
  # @see Puma::Cluster
680
690
  #
681
691
  def workers(count)
682
- @options[:workers] = count.to_i
692
+ @options[:workers] = count.nil? ? 0 : @config.send(:parse_workers, count)
683
693
  end
684
694
 
685
695
  # Disable warning message when running in cluster mode with a single worker.
@@ -818,6 +828,20 @@ module Puma
818
828
 
819
829
  alias_method :after_worker_boot, :after_worker_fork
820
830
 
831
+ # Code to run in the master right after a worker has stopped. The worker's
832
+ # index and Process::Status are passed as arguments.
833
+ #
834
+ # @note Cluster mode only.
835
+ #
836
+ # @example
837
+ # after_worker_shutdown do |worker_handle|
838
+ # puts 'Worker crashed' unless worker_handle.process_status.success?
839
+ # end
840
+ #
841
+ def after_worker_shutdown(&block)
842
+ process_hook :after_worker_shutdown, nil, block, cluster_only: true
843
+ end
844
+
821
845
  # Code to run after puma is booted (works for both single and cluster modes).
822
846
  #
823
847
  # @example
@@ -980,6 +1004,7 @@ module Puma
980
1004
  # The default is +true+ if your app uses more than 1 worker.
981
1005
  #
982
1006
  # @note Cluster mode only.
1007
+ # @note When using `fork_worker`, this only applies to worker 0.
983
1008
  #
984
1009
  # @example
985
1010
  # preload_app!
@@ -1207,13 +1232,19 @@ module Puma
1207
1232
  end
1208
1233
 
1209
1234
 
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
1235
+ # Maximum delay of worker accept loop.
1236
+ #
1237
+ # Attempts to route traffic to less-busy workers by causing a busy worker to delay
1238
+ # listening on the socket, allowing workers which are not processing as many
1212
1239
  # requests to pick up new requests first.
1213
1240
  #
1214
1241
  # The default is 0.005 seconds.
1215
1242
  #
1216
- # Only works on MRI. For all other interpreters, this setting does nothing.
1243
+ # To turn off this feature, set the value to 0.
1244
+ #
1245
+ # @note Cluster mode with >= 2 workers only.
1246
+ #
1247
+ # @note Interpreters with forking support only.
1217
1248
  #
1218
1249
  # @see Puma::Server#handle_servers
1219
1250
  # @see Puma::ThreadPool#wait_for_less_busy_worker
@@ -1357,7 +1388,7 @@ module Puma
1357
1388
  #
1358
1389
  # The default is +:auto+.
1359
1390
  #
1360
- # @see https://github.com/socketry/nio4r/blob/master/lib/nio/selector.rb
1391
+ # @see https://github.com/socketry/nio4r/blob/main/lib/nio/selector.rb
1361
1392
  #
1362
1393
  def io_selector_backend(backend)
1363
1394
  @options[:io_selector_backend] = backend.to_sym
data/lib/puma/launcher.rb CHANGED
@@ -42,26 +42,40 @@ module Puma
42
42
  # end
43
43
  # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
44
44
  def initialize(conf, launcher_args={})
45
- @runner = nil
46
- @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
47
- @events = launcher_args[:events] || Events.new
48
- @argv = launcher_args[:argv] || []
49
- @original_argv = @argv.dup
50
- @config = conf
51
-
52
- env = launcher_args.delete(:env) || ENV
45
+ ## Minimal initialization before potential early restart (e.g. from bundle pruning)
53
46
 
47
+ @config = conf
48
+ # Advertise the CLI Configuration before config files are loaded
49
+ Puma.cli_config = @config if defined?(Puma.cli_config)
54
50
  @config.clamp
51
+
55
52
  @options = @config.options
56
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]
57
58
  @options[:log_writer] = @log_writer
58
59
  @options[:logger] = @log_writer if clustered?
59
60
 
60
- # Advertise the Configuration
61
- Puma.cli_config = @config if defined?(Puma.cli_config)
61
+ @events = launcher_args[:events] || Events.new
62
+
63
+ @argv = launcher_args[:argv] || []
64
+ @original_argv = @argv.dup
65
+
66
+ ## End minimal initialization
67
+
68
+ generate_restart_data
69
+ Dir.chdir(@restart_dir)
70
+
71
+ prune_bundler!
72
+
73
+ env = launcher_args.delete(:env) || ENV
74
+
75
+ # Log after prune_bundler! to avoid duplicate logging if a restart occurs
62
76
  log_config if env['PUMA_LOG_CONFIG']
63
77
 
64
- @binder = Binder.new(@log_writer, @options)
78
+ @binder = Binder.new(@log_writer, @options)
65
79
  @binder.create_inherited_fds(env).each { |k| env.delete k }
66
80
  @binder.create_activated_fds(env).each { |k| env.delete k }
67
81
 
@@ -81,21 +95,10 @@ module Puma
81
95
  )
82
96
  end
83
97
 
84
- @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
85
- @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
86
-
87
- @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
88
-
89
- generate_restart_data
90
-
91
98
  if clustered? && !Puma.forkable?
92
99
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
93
100
  end
94
101
 
95
- Dir.chdir(@restart_dir)
96
-
97
- prune_bundler!
98
-
99
102
  @environment = @options[:environment] if @options[:environment]
100
103
  set_rack_environment
101
104
 
@@ -139,7 +142,10 @@ module Puma
139
142
  # Delete the configured pidfile
140
143
  def delete_pidfile
141
144
  path = @options[:pidfile]
142
- File.unlink(path) if path && File.exist?(path)
145
+ begin
146
+ File.unlink(path) if path
147
+ rescue Errno::ENOENT
148
+ end
143
149
  end
144
150
 
145
151
  # Begin async shutdown of the server
@@ -381,9 +387,9 @@ module Puma
381
387
  # using it.
382
388
  @restart_dir = Dir.pwd
383
389
 
384
- # Use the same trick as unicorn, namely favor PWD because
385
- # it will contain an unresolved symlink, useful for when
386
- # 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.
387
393
  elsif dir = ENV['PWD']
388
394
  s_env = File.stat(dir)
389
395
  s_pwd = File.stat(Dir.pwd)
data/lib/puma/reactor.rb CHANGED
@@ -75,15 +75,12 @@ module Puma
75
75
  private
76
76
 
77
77
  def select_loop
78
- close_selector = true
79
78
  begin
80
79
  until @input.closed? && @input.empty?
81
80
  # Wakeup any registered object that receives incoming data.
82
81
  # Block until the earliest timeout or Selector#wakeup is called.
83
82
  timeout = (earliest = @timeouts.first) && earliest.timeout
84
- monitor_wake_up = false
85
83
  @selector.select(timeout) do |monitor|
86
- monitor_wake_up = true
87
84
  wakeup!(monitor.value)
88
85
  end
89
86
 
@@ -103,18 +100,12 @@ module Puma
103
100
  STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})"
104
101
  STDERR.puts e.backtrace
105
102
 
106
- # NoMethodError may be rarely raised when calling @selector.select, which
107
- # is odd. Regardless, it may continue for thousands of calls if retried.
108
- # Also, when it raises, @selector.close also raises an error.
109
- if !monitor_wake_up && NoMethodError === e
110
- close_selector = false
111
- else
112
- retry
113
- end
103
+ retry
114
104
  end
105
+
115
106
  # Wakeup all remaining objects on shutdown.
116
107
  @timeouts.each(&@block)
117
- @selector.close if close_selector
108
+ @selector.close
118
109
  end
119
110
 
120
111
  # Start monitoring the object.
data/lib/puma/request.rb CHANGED
@@ -36,17 +36,19 @@ 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
@@ -54,7 +56,7 @@ module Puma
54
56
  app_body = nil
55
57
  error = nil
56
58
 
57
- return false if closed_socket?(socket)
59
+ return :close if closed_socket?(socket)
58
60
 
59
61
  if client.http_content_length_limit_exceeded
60
62
  return prepare_response(413, {}, ["Payload Too Large"], requests, client)
@@ -167,13 +169,13 @@ module Puma
167
169
  # a call to `Server#lowlevel_error`
168
170
  # @param requests [Integer] number of inline requests handled
169
171
  # @param client [Puma::Client]
170
- # @return [Boolean,:async] keep-alive status or `:async`
172
+ # @return [:close, :keep_alive, :async]
171
173
  def prepare_response(status, headers, res_body, requests, client)
172
174
  env = client.env
173
175
  socket = client.io
174
176
  io_buffer = client.io_buffer
175
177
 
176
- return false if closed_socket?(socket)
178
+ return :close if closed_socket?(socket)
177
179
 
178
180
  # Close the connection after a reasonable number of inline requests
179
181
  force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
@@ -244,7 +246,7 @@ module Puma
244
246
  io_buffer << LINE_END
245
247
  fast_write_str socket, io_buffer.read_and_reset
246
248
  socket.flush
247
- return keep_alive
249
+ return keep_alive ? :keep_alive : :close
248
250
  end
249
251
  else
250
252
  if content_length
@@ -270,7 +272,7 @@ module Puma
270
272
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
271
273
  body.close if close_body
272
274
  # if we're shutting down, close keep_alive connections
273
- !shutting_down? && keep_alive
275
+ !shutting_down? && keep_alive ? :keep_alive : :close
274
276
  end
275
277
 
276
278
  HTTP_ON_VALUES = { "on" => true, HTTPS => true }
data/lib/puma/runner.rb CHANGED
@@ -70,7 +70,7 @@ module Puma
70
70
  token = nil if token.empty? || token == 'none'
71
71
  end
72
72
 
73
- app = Puma::App::Status.new @launcher, token
73
+ app = Puma::App::Status.new @launcher, token: token, data_only: @options[:control_data_only]
74
74
 
75
75
  # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
76
76
  # Use `nil` for events, no hooks in control server
data/lib/puma/server.rb CHANGED
@@ -19,9 +19,6 @@ require 'socket'
19
19
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
20
20
 
21
21
  module Puma
22
- # Add `Thread#puma_server` and `Thread#puma_server=`
23
- Thread.attr_accessor(:puma_server)
24
-
25
22
  # The HTTP Server itself. Serves out a single Rack app.
26
23
  #
27
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -262,7 +259,7 @@ module Puma
262
259
 
263
260
  @status = :run
264
261
 
265
- @thread_pool = ThreadPool.new(thread_name, options) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) { |client| process_client client }
266
263
 
267
264
  if @queue_requests
268
265
  @reactor = Reactor.new(@io_selector_backend) { |c|
@@ -302,7 +299,7 @@ module Puma
302
299
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
303
300
  # such as nginx) then the application would be subject to a slow client attack.
304
301
  #
305
- # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
302
+ # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/main/docs/architecture.md).
306
303
  #
307
304
  # The method checks to see if it has the full header and body with
308
305
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
@@ -481,9 +478,6 @@ module Puma
481
478
  #
482
479
  # Return true if one or more requests were processed.
483
480
  def process_client(client)
484
- # Advertise this server into the thread
485
- Thread.current.puma_server = self
486
-
487
481
  close_socket = true
488
482
 
489
483
  requests = 0
@@ -502,31 +496,41 @@ module Puma
502
496
  client.finish(@first_data_timeout)
503
497
  end
504
498
 
505
- @requests_count += 1
506
- case handle_request(client, requests + 1)
507
- when false
508
- when :async
509
- close_socket = false
510
- when true
511
- requests += 1
499
+ can_loop = true
500
+ while can_loop
501
+ can_loop = false
502
+ @requests_count += 1
503
+ case handle_request(client, requests + 1)
504
+ when :close
505
+ when :async
506
+ close_socket = false
507
+ when :keep_alive
508
+ requests += 1
512
509
 
513
- client.reset
510
+ client.reset
514
511
 
515
- # This indicates data exists in the client read buffer and there may be
516
- # additional requests on it, so process them
517
- next_request_ready = if client.has_back_to_back_requests?
518
- with_force_shutdown(client) { client.process_back_to_back_requests }
519
- else
520
- with_force_shutdown(client) { client.eagerly_finish }
521
- end
512
+ # This indicates data exists in the client read buffer and there may be
513
+ # additional requests on it, so process them
514
+ next_request_ready = if client.has_back_to_back_requests?
515
+ with_force_shutdown(client) { client.process_back_to_back_requests }
516
+ else
517
+ with_force_shutdown(client) { client.eagerly_finish }
518
+ end
522
519
 
523
- if next_request_ready
524
- @thread_pool << client
525
- close_socket = false
526
- elsif @queue_requests
527
- client.set_timeout @persistent_timeout
528
- if @reactor.add client
529
- close_socket = false
520
+ if next_request_ready
521
+ # When Puma has spare threads, allow this one to be monopolized
522
+ # Perf optimization for https://github.com/puma/puma/issues/3788
523
+ if @thread_pool.waiting > 0
524
+ can_loop = true
525
+ else
526
+ @thread_pool << client
527
+ close_socket = false
528
+ end
529
+ elsif @queue_requests
530
+ client.set_timeout @persistent_timeout
531
+ if @reactor.add client
532
+ close_socket = false
533
+ end
530
534
  end
531
535
  end
532
536
  end
@@ -706,7 +710,7 @@ module Puma
706
710
 
707
711
  def reset_max
708
712
  @reactor.reactor_max = 0 if @reactor
709
- @thread_pool.reset_max
713
+ @thread_pool&.reset_max
710
714
  end
711
715
 
712
716
  # below are 'delegations' to binder
data/lib/puma/single.rb CHANGED
@@ -49,8 +49,8 @@ module Puma
49
49
 
50
50
  start_control
51
51
 
52
- @server = server = start_server
53
- server_thread = server.run
52
+ @server = start_server
53
+ server_thread = @server.run
54
54
 
55
55
  log "Use Ctrl-C to stop"
56
56
 
@@ -32,10 +32,11 @@ module Puma
32
32
  "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
33
  end
34
34
  end
35
+
35
36
  if permission
36
- File.write path, contents, mode: 'wb:UTF-8'
37
- else
38
37
  File.write path, contents, mode: 'wb:UTF-8', perm: permission
38
+ else
39
+ File.write path, contents, mode: 'wb:UTF-8'
39
40
  end
40
41
  end
41
42
 
@@ -5,6 +5,10 @@ require 'thread'
5
5
  require_relative 'io_buffer'
6
6
 
7
7
  module Puma
8
+
9
+ # Add `Thread#puma_server` and `Thread#puma_server=`
10
+ Thread.attr_accessor(:puma_server)
11
+
8
12
  # Internal Docs for A simple thread pool management object.
9
13
  #
10
14
  # Each Puma "worker" has a thread pool to process requests.
@@ -33,7 +37,9 @@ module Puma
33
37
  # The block passed is the work that will be performed in each
34
38
  # thread.
35
39
  #
36
- def initialize(name, options = {}, &block)
40
+ def initialize(name, options = {}, server: nil, &block)
41
+ @server = server
42
+
37
43
  @not_empty = ConditionVariable.new
38
44
  @not_full = ConditionVariable.new
39
45
  @mutex = Mutex.new
@@ -134,6 +140,9 @@ module Puma
134
140
  trigger_before_thread_start_hooks
135
141
  th = Thread.new(@spawned) do |spawned|
136
142
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
143
+ # Advertise server into the thread
144
+ Thread.current.puma_server = @server
145
+
137
146
  todo = @todo
138
147
  block = @block
139
148
  mutex = @mutex