puma 7.0.4-java → 7.1.0-java

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 504463ef9ae6d0f72e563929b1fffc93a8dae6a81ba26a8d2ad2d11ca6cf74ac
4
- data.tar.gz: ffc359bcb19e5ec013fea326e49f877ed02460e0501cc4cb3e397741b0858b69
3
+ metadata.gz: c34ad6eb102937d5162b4ee769397333da4ad801d17fab6cc70226319ad94db2
4
+ data.tar.gz: 4c4fd1e10d19a1a156a8cd524bcf547e7b0c59cbac7868a21c3cd469ca60c96d
5
5
  SHA512:
6
- metadata.gz: 2b32465bffbdd70561c6220e8f01539f4eeb821fa081c10623698ffa770ac5bd4fbc727da3a038c522c0a3d152bba9e6d4a3dea30d45d3d3b0244fdde3dd8fb5
7
- data.tar.gz: 7c955cc7fe61bda55ba381d7af9d2b2ee7166f30d534f26ee9b955985c36672857eb39e2e487a2a481007f7b75e65a87a208fc9ea8746bdd4c687547d45a9405
6
+ metadata.gz: 5f1f1026adb88d8bae2fb4008a94731b190af258c68270ab4f1c7578457f260612982d102c19b302b2f724b5e8bed7e774a6a3b8effd79d1ed23afa5349928cb
7
+ data.tar.gz: f01f2c0187729869c45940da06a5a496eaf404889baa0ee4cacbfb704a286b5d2f0852ffb0cf7671a2db8a5263850b83d4d381dc689f05ded5190c27b234afa8
data/History.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 7.1.0 / 2025-10-16
2
+
3
+ * Features
4
+ * Introduce `after_worker_shutdown` hook ([#3707])
5
+ * Reintroduce keepalive "fast inline" behavior. Provides faster (8x on JRuby & 1.4x on Ruby) pipeline processing ([#3794])
6
+
7
+ * Bugfixes
8
+ * Skip reading zero bytes when request body is buffered ([#3795])
9
+ * Fix `PUMA_LOG_CONFIG=1` logging twice with prune_bundler enabled ([#3778])
10
+ * Fix prune_bundler not showing in `PUMA_LOG_CONFIG=1` output ([#3779])
11
+ * Guard ThreadPool method call, which may be nil during shutdown ([#3791], [#3790])
12
+ * Set `Thread.current.puma_server` in Thread init code, not every request ([#3774])
13
+ * Fix race condition while deleting pidfile ([#3657])
14
+
1
15
  ## 7.0.4 / 2025-09-23
2
16
 
3
17
  * Bugfixes
@@ -2245,6 +2259,15 @@ be added back in a future date when a java Puma::MiniSSL is added.
2245
2259
  * Bugfixes
2246
2260
  * Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
2247
2261
 
2262
+ [#3707]:https://github.com/puma/puma/pull/3707 "PR by @nerdrew, merged 2025-10-02"
2263
+ [#3794]:https://github.com/puma/puma/pull/3794 "PR by @schneems, merged 2025-10-16"
2264
+ [#3795]:https://github.com/puma/puma/pull/3795 "PR by @MSP-Greg, merged 2025-10-16"
2265
+ [#3778]:https://github.com/puma/puma/pull/3778 "PR by @joshuay03, merged 2025-10-16"
2266
+ [#3779]:https://github.com/puma/puma/pull/3779 "PR by @joshuay03, merged 2025-10-16"
2267
+ [#3791]:https://github.com/puma/puma/pull/3791 "PR by @MSP-Greg, merged 2025-10-09"
2268
+ [#3790]:https://github.com/puma/puma/issues/3790 "Issue by @eric-wtfoxtrot, closed 2025-10-09"
2269
+ [#3774]:https://github.com/puma/puma/pull/3774 "PR by @MSP-Greg, merged 2025-10-16"
2270
+ [#3657]:https://github.com/puma/puma/pull/3657 "PR by @marksmith, merged 2025-10-16"
2248
2271
  [#3703]:https://github.com/puma/puma/pull/3703 "PR by @marshall-lee, merged 2025-09-20"
2249
2272
  [#3742]:https://github.com/puma/puma/pull/3742 "PR by @kenballus, merged 2025-09-18"
2250
2273
  [#3754]:https://github.com/puma/puma/pull/3754 "PR by @byroot, merged 2025-09-18"
data/README.md CHANGED
@@ -142,8 +142,8 @@ Preloading can’t be used with phased restart, since phased restart kills and r
142
142
 
143
143
  #### Cluster mode hooks
144
144
 
145
- When using clustered mode, Puma's configuration DSL provides `before_fork` and `before_worker_boot`
146
- hooks to run code when the master process forks and child workers are booted respectively.
145
+ When using clustered mode, Puma's configuration DSL provides `before_fork`, `before_worker_boot`, and `after_worker_shutdown`
146
+ hooks to run code when the master process forks, the child workers are booted, and after each child worker exits respectively.
147
147
 
148
148
  It is recommended to use these hooks with `preload_app!`, otherwise constants loaded by your
149
149
  application (such as `Rails`) will not be available inside the hooks.
@@ -157,6 +157,11 @@ end
157
157
  before_worker_boot do
158
158
  # Add code to run inside the Puma worker process after forking.
159
159
  end
160
+
161
+ after_worker_shutdown do |worker_handle|
162
+ # Add code to run inside the Puma master process after a worker exits. `worker.process_status` can be used to get the
163
+ # `Process::Status` of the exited worker.
164
+ end
160
165
  ```
161
166
 
162
167
  In addition, there is an `before_refork` and `after_refork` hooks which are used only in [`fork_worker` mode](docs/fork_worker.md),
data/docs/kubernetes.md CHANGED
@@ -10,7 +10,7 @@ Assuming you already have a running cluster and docker image repository, you can
10
10
 
11
11
  A basic Dockerfile example:
12
12
 
13
- ```
13
+ ```Dockerfile
14
14
  FROM ruby:3.4.5-alpine # can be updated to newer ruby versions
15
15
  RUN apk update && apk add build-base # and any other packages you need
16
16
 
@@ -28,7 +28,7 @@ CMD bundle exec rackup -o 0.0.0.0
28
28
 
29
29
  A sample `deployment.yaml`:
30
30
 
31
- ```
31
+ ```yaml
32
32
  ---
33
33
  apiVersion: apps/v1
34
34
  kind: Deployment
data/docs/stats.md CHANGED
@@ -62,7 +62,7 @@ When Puma runs in single mode, these stats are available at the top level. When
62
62
  this is a "wholistic" stat reflecting the overall current state of work to be done and the capacity to do it.
63
63
  * pool_capacity: `how many threads are waiting to receive work` + `max_threads` - `running`. In a typical configuration where `min_threads`
64
64
  and `max_threads` are configured to the same number, this is simply `how many threads are waiting to receive work`. This number exists only as a stat
65
- and is not used for any internal decisions, unlike `busy_theads`, which is usually a more useful stat.
65
+ and is not used for any internal decisions, unlike `busy_threads`, which is usually a more useful stat.
66
66
  * max_threads: the maximum number of threads Puma is configured to spool per worker
67
67
  * requests_count: the number of requests this worker has served since starting
68
68
  * reactor_max: the maximum observed number of requests held in Puma's "reactor" which is used for asyncronously buffering request bodies. This stat is reset on every call, so it's the maximum value observed since the last stat call.
data/lib/puma/client.rb CHANGED
@@ -500,40 +500,36 @@ module Puma
500
500
  # after this
501
501
  remain = @body_remain
502
502
 
503
- if remain > CHUNK_SIZE
504
- want = CHUNK_SIZE
505
- else
506
- want = remain
507
- end
503
+ # don't bother with reading zero bytes
504
+ unless remain.zero?
505
+ begin
506
+ chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
507
+ rescue IO::WaitReadable
508
+ return false
509
+ rescue SystemCallError, IOError
510
+ raise ConnectionError, "Connection error detected during read"
511
+ end
508
512
 
509
- begin
510
- chunk = @io.read_nonblock(want, @read_buffer)
511
- rescue IO::WaitReadable
512
- return false
513
- rescue SystemCallError, IOError
514
- raise ConnectionError, "Connection error detected during read"
515
- end
513
+ # No chunk means a closed socket
514
+ unless chunk
515
+ @body.close
516
+ @buffer = nil
517
+ set_ready
518
+ raise EOFError
519
+ end
516
520
 
517
- # No chunk means a closed socket
518
- unless chunk
519
- @body.close
520
- @buffer = nil
521
- set_ready
522
- raise EOFError
521
+ remain -= @body.write(chunk)
523
522
  end
524
523
 
525
- remain -= @body.write(chunk)
526
-
527
524
  if remain <= 0
528
525
  @body.rewind
529
526
  @buffer = nil
530
527
  set_ready
531
- return true
528
+ true
529
+ else
530
+ @body_remain = remain
531
+ false
532
532
  end
533
-
534
- @body_remain = remain
535
-
536
- false
537
533
  end
538
534
 
539
535
  def read_chunked_body
@@ -28,10 +28,10 @@ module Puma
28
28
  @worker_max = Array.new WORKER_MAX_KEYS.length, 0
29
29
  end
30
30
 
31
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
31
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at, :process_status
32
32
 
33
33
  # @version 5.0.0
34
- attr_writer :pid, :phase
34
+ attr_writer :pid, :phase, :process_status
35
35
 
36
36
  def booted?
37
37
  @stage == :booted
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.
@@ -238,7 +238,7 @@ module Puma
238
238
  def phased_restart(refork = false)
239
239
  return false if @options[:preload_app] && !refork
240
240
 
241
- @phased_restart = refork ? :refork : true
241
+ @pending_phased_restart = refork ? :refork : true
242
242
  wakeup!
243
243
 
244
244
  true
@@ -456,11 +456,11 @@ module Puma
456
456
  break
457
457
  end
458
458
 
459
- if @phased_restart
460
- start_phased_restart(@phased_restart == :refork)
459
+ if @pending_phased_restart
460
+ start_phased_restart(@pending_phased_restart == :refork)
461
461
 
462
- in_phased_restart = @phased_restart
463
- @phased_restart = false
462
+ in_phased_restart = @pending_phased_restart
463
+ @pending_phased_restart = false
464
464
 
465
465
  workers_not_booted = @options[:workers]
466
466
  # worker 0 is not restarted on refork
@@ -583,7 +583,9 @@ module Puma
583
583
  # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
584
584
  # 2. When `fork_worker` is enabled, some worker may not be direct children,
585
585
  # 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)
586
+ if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
587
+ w.process_status = status
588
+ @config.run_hooks(:after_worker_shutdown, w, @log_writer)
587
589
  true
588
590
  else
589
591
  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
@@ -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,
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.1.0"
104
+ CODE_NAME = "Neon Witch"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
data/lib/puma/dsl.rb CHANGED
@@ -656,7 +656,8 @@ module Puma
656
656
  @options[:state] = path.to_s
657
657
  end
658
658
 
659
- # Use +permission+ to restrict permissions for the state file.
659
+ # Use +permission+ to restrict permissions for the state file. By convention,
660
+ # +permission+ is an octal number (e.g. `0640` or `0o640`).
660
661
  #
661
662
  # @example
662
663
  # state_permission 0600
@@ -818,6 +819,20 @@ module Puma
818
819
 
819
820
  alias_method :after_worker_boot, :after_worker_fork
820
821
 
822
+ # Code to run in the master right after a worker has stopped. The worker's
823
+ # index and Process::Status are passed as arguments.
824
+ #
825
+ # @note Cluster mode only.
826
+ #
827
+ # @example
828
+ # after_worker_shutdown do |worker_handle|
829
+ # puts 'Worker crashed' unless worker_handle.process_status.success?
830
+ # end
831
+ #
832
+ def after_worker_shutdown(&block)
833
+ process_hook :after_worker_shutdown, nil, block, cluster_only: true
834
+ end
835
+
821
836
  # Code to run after puma is booted (works for both single and cluster modes).
822
837
  #
823
838
  # @example
@@ -1207,13 +1222,19 @@ module Puma
1207
1222
  end
1208
1223
 
1209
1224
 
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
1225
+ # Maximum delay of worker accept loop.
1226
+ #
1227
+ # Attempts to route traffic to less-busy workers by causing a busy worker to delay
1228
+ # listening on the socket, allowing workers which are not processing as many
1212
1229
  # requests to pick up new requests first.
1213
1230
  #
1214
1231
  # The default is 0.005 seconds.
1215
1232
  #
1216
- # Only works on MRI. For all other interpreters, this setting does nothing.
1233
+ # To turn off this feature, set the value to 0.
1234
+ #
1235
+ # @note Cluster mode with >= 2 workers only.
1236
+ #
1237
+ # @note Interpreters with forking support only.
1217
1238
  #
1218
1239
  # @see Puma::Server#handle_servers
1219
1240
  # @see Puma::ThreadPool#wait_for_less_busy_worker
data/lib/puma/launcher.rb CHANGED
@@ -42,26 +42,39 @@ 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 for a potential early restart (e.g. when pruning bundle)
53
46
 
47
+ @config = conf
54
48
  @config.clamp
49
+
55
50
  @options = @config.options
56
51
 
52
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
53
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
54
+ @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
55
+ @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
57
56
  @options[:log_writer] = @log_writer
58
57
  @options[:logger] = @log_writer if clustered?
59
58
 
59
+ @events = launcher_args[:events] || Events.new
60
+
61
+ @argv = launcher_args[:argv] || []
62
+ @original_argv = @argv.dup
63
+
64
+ ## End minimal initialization
65
+
66
+ generate_restart_data
67
+ Dir.chdir(@restart_dir)
68
+
69
+ prune_bundler!
70
+
71
+ env = launcher_args.delete(:env) || ENV
72
+
60
73
  # Advertise the Configuration
61
74
  Puma.cli_config = @config if defined?(Puma.cli_config)
62
75
  log_config if env['PUMA_LOG_CONFIG']
63
76
 
64
- @binder = Binder.new(@log_writer, @options)
77
+ @binder = Binder.new(@log_writer, @options)
65
78
  @binder.create_inherited_fds(env).each { |k| env.delete k }
66
79
  @binder.create_activated_fds(env).each { |k| env.delete k }
67
80
 
@@ -81,21 +94,10 @@ module Puma
81
94
  )
82
95
  end
83
96
 
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
97
  if clustered? && !Puma.forkable?
92
98
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
93
99
  end
94
100
 
95
- Dir.chdir(@restart_dir)
96
-
97
- prune_bundler!
98
-
99
101
  @environment = @options[:environment] if @options[:environment]
100
102
  set_rack_environment
101
103
 
@@ -139,7 +141,10 @@ module Puma
139
141
  # Delete the configured pidfile
140
142
  def delete_pidfile
141
143
  path = @options[:pidfile]
142
- File.unlink(path) if path && File.exist?(path)
144
+ begin
145
+ File.unlink(path) if path
146
+ rescue Errno::ENOENT
147
+ end
143
148
  end
144
149
 
145
150
  # Begin async shutdown of the server
@@ -381,9 +386,9 @@ module Puma
381
386
  # using it.
382
387
  @restart_dir = Dir.pwd
383
388
 
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.
389
+ # Use the same trick as unicorn, namely favor PWD because
390
+ # it will contain an unresolved symlink, useful for when
391
+ # the pwd is /data/releases/current.
387
392
  elsif dir = ENV['PWD']
388
393
  s_env = File.stat(dir)
389
394
  s_pwd = File.stat(Dir.pwd)
Binary file
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|
@@ -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 false
505
+ when :async
506
+ close_socket = false
507
+ when true
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
@@ -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
metadata CHANGED
@@ -1,21 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.4
4
+ version: 7.1.0
5
5
  platform: java
6
6
  authors:
7
7
  - Evan Phoenix
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-23 00:00:00.000000000 Z
10
+ date: 2025-10-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: nio4r
14
13
  requirement: !ruby/object:Gem::Requirement
15
14
  requirements:
16
15
  - - "~>"
17
16
  - !ruby/object:Gem::Version
18
17
  version: '2.0'
18
+ name: nio4r
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
@@ -132,6 +132,7 @@ metadata:
132
132
  homepage_uri: https://puma.io
133
133
  source_code_uri: https://github.com/puma/puma
134
134
  rubygems_mfa_required: 'true'
135
+ msys2_mingw_dependencies: openssl
135
136
  rdoc_options: []
136
137
  require_paths:
137
138
  - lib