puma 6.4.1 → 7.2.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +407 -8
  3. data/README.md +109 -49
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +11 -1
  6. data/docs/java_options.md +54 -0
  7. data/docs/jungle/README.md +1 -1
  8. data/docs/kubernetes.md +11 -16
  9. data/docs/plugins.md +6 -2
  10. data/docs/restart.md +2 -2
  11. data/docs/signals.md +21 -21
  12. data/docs/stats.md +11 -5
  13. data/docs/systemd.md +14 -5
  14. data/ext/puma_http11/extconf.rb +20 -32
  15. data/ext/puma_http11/mini_ssl.c +29 -9
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +40 -9
  17. data/ext/puma_http11/puma_http11.c +125 -118
  18. data/lib/puma/app/status.rb +11 -3
  19. data/lib/puma/binder.rb +21 -11
  20. data/lib/puma/cli.rb +10 -8
  21. data/lib/puma/client.rb +183 -83
  22. data/lib/puma/cluster/worker.rb +24 -21
  23. data/lib/puma/cluster/worker_handle.rb +38 -8
  24. data/lib/puma/cluster.rb +73 -47
  25. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  26. data/lib/puma/commonlogger.rb +3 -3
  27. data/lib/puma/configuration.rb +131 -60
  28. data/lib/puma/const.rb +31 -12
  29. data/lib/puma/control_cli.rb +10 -6
  30. data/lib/puma/detect.rb +2 -0
  31. data/lib/puma/dsl.rb +411 -121
  32. data/lib/puma/error_logger.rb +7 -5
  33. data/lib/puma/events.rb +25 -10
  34. data/lib/puma/io_buffer.rb +8 -4
  35. data/lib/puma/jruby_restart.rb +0 -16
  36. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  37. data/lib/puma/launcher.rb +73 -55
  38. data/lib/puma/log_writer.rb +9 -9
  39. data/lib/puma/minissl/context_builder.rb +1 -0
  40. data/lib/puma/minissl.rb +1 -1
  41. data/lib/puma/null_io.rb +26 -0
  42. data/lib/puma/plugin/systemd.rb +3 -3
  43. data/lib/puma/rack/urlmap.rb +1 -1
  44. data/lib/puma/reactor.rb +19 -13
  45. data/lib/puma/request.rb +71 -39
  46. data/lib/puma/runner.rb +15 -17
  47. data/lib/puma/sd_notify.rb +1 -4
  48. data/lib/puma/server.rb +134 -73
  49. data/lib/puma/single.rb +7 -4
  50. data/lib/puma/state_file.rb +3 -2
  51. data/lib/puma/thread_pool.rb +57 -80
  52. data/lib/puma/util.rb +0 -7
  53. data/lib/puma.rb +10 -0
  54. data/lib/rack/handler/puma.rb +10 -7
  55. data/tools/Dockerfile +15 -5
  56. metadata +14 -15
  57. data/ext/puma_http11/ext_help.h +0 -15
@@ -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.
@@ -25,19 +29,23 @@ module Puma
25
29
  # up its work before leaving the thread to die on the vine.
26
30
  SHUTDOWN_GRACE_TIME = 5 # seconds
27
31
 
32
+ attr_reader :out_of_band_running
33
+
28
34
  # Maintain a minimum of +min+ and maximum of +max+ threads
29
35
  # in the pool.
30
36
  #
31
37
  # The block passed is the work that will be performed in each
32
38
  # thread.
33
39
  #
34
- def initialize(name, options = {}, &block)
40
+ def initialize(name, options = {}, server: nil, &block)
41
+ @server = server
42
+
35
43
  @not_empty = ConditionVariable.new
36
44
  @not_full = ConditionVariable.new
37
45
  @mutex = Mutex.new
46
+ @todo = Queue.new
38
47
 
39
- @todo = []
40
-
48
+ @backlog_max = 0
41
49
  @spawned = 0
42
50
  @waiting = 0
43
51
 
@@ -50,7 +58,8 @@ module Puma
50
58
  @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
51
59
  @block = block
52
60
  @out_of_band = options[:out_of_band]
53
- @clean_thread_locals = options[:clean_thread_locals]
61
+ @out_of_band_running = false
62
+ @out_of_band_condvar = ConditionVariable.new
54
63
  @before_thread_start = options[:before_thread_start]
55
64
  @before_thread_exit = options[:before_thread_exit]
56
65
  @reaping_time = options[:reaping_time]
@@ -79,18 +88,37 @@ module Puma
79
88
 
80
89
  attr_reader :spawned, :trim_requested, :waiting
81
90
 
82
- def self.clean_thread_locals
83
- Thread.current.keys.each do |key| # rubocop: disable Style/HashEachMethods
84
- Thread.current[key] = nil unless key == :__recursive_key__
91
+ # generate stats hash so as not to perform multiple locks
92
+ # @return [Hash] hash containing stat info from ThreadPool
93
+ def stats
94
+ with_mutex do
95
+ temp = @backlog_max
96
+ @backlog_max = 0
97
+ { backlog: @todo.size,
98
+ running: @spawned,
99
+ pool_capacity: @waiting + (@max - @spawned),
100
+ busy_threads: @spawned - @waiting + @todo.size,
101
+ backlog_max: temp
102
+ }
85
103
  end
86
104
  end
87
105
 
106
+ def reset_max
107
+ with_mutex { @backlog_max = 0 }
108
+ end
109
+
88
110
  # How many objects have yet to be processed by the pool?
89
111
  #
90
112
  def backlog
91
113
  with_mutex { @todo.size }
92
114
  end
93
115
 
116
+ # The maximum size of the backlog
117
+ #
118
+ def backlog_max
119
+ with_mutex { @backlog_max }
120
+ end
121
+
94
122
  # @!attribute [r] pool_capacity
95
123
  def pool_capacity
96
124
  waiting + (@max - spawned)
@@ -112,6 +140,9 @@ module Puma
112
140
  trigger_before_thread_start_hooks
113
141
  th = Thread.new(@spawned) do |spawned|
114
142
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
143
+ # Advertise server into the thread
144
+ Thread.current.puma_server = @server
145
+
115
146
  todo = @todo
116
147
  block = @block
117
148
  mutex = @mutex
@@ -147,10 +178,6 @@ module Puma
147
178
  work = todo.shift
148
179
  end
149
180
 
150
- if @clean_thread_locals
151
- ThreadPool.clean_thread_locals
152
- end
153
-
154
181
  begin
155
182
  @out_of_band_pending = true if block.call(work)
156
183
  rescue Exception => e
@@ -171,7 +198,7 @@ module Puma
171
198
 
172
199
  @before_thread_start.each do |b|
173
200
  begin
174
- b.call
201
+ b[:block].call
175
202
  rescue Exception => e
176
203
  STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
177
204
  end
@@ -186,7 +213,7 @@ module Puma
186
213
 
187
214
  @before_thread_exit.each do |b|
188
215
  begin
189
- b.call
216
+ b[:block].call
190
217
  rescue Exception => e
191
218
  STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
192
219
  end
@@ -202,16 +229,27 @@ module Puma
202
229
 
203
230
  # we execute on idle hook when all threads are free
204
231
  return false unless @spawned == @waiting
205
-
206
- @out_of_band.each(&:call)
232
+ @out_of_band_running = true
233
+ @out_of_band.each { |b| b[:block].call }
207
234
  true
208
235
  rescue Exception => e
209
236
  STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
210
237
  true
238
+ ensure
239
+ @out_of_band_running = false
240
+ @out_of_band_condvar.broadcast
211
241
  end
212
242
 
213
243
  private :trigger_out_of_band_hook
214
244
 
245
+ def wait_while_out_of_band_running
246
+ return unless @out_of_band_running
247
+
248
+ with_mutex do
249
+ @out_of_band_condvar.wait(@mutex) while @out_of_band_running
250
+ end
251
+ end
252
+
215
253
  # @version 5.0.0
216
254
  def with_mutex(&block)
217
255
  @mutex.owned? ?
@@ -227,6 +265,8 @@ module Puma
227
265
  end
228
266
 
229
267
  @todo << work
268
+ t = @todo.size
269
+ @backlog_max = t if t > @backlog_max
230
270
 
231
271
  if @waiting < @todo.size and @spawned < @max
232
272
  spawn_thread
@@ -236,69 +276,6 @@ module Puma
236
276
  end
237
277
  end
238
278
 
239
- # This method is used by `Puma::Server` to let the server know when
240
- # the thread pool can pull more requests from the socket and
241
- # pass to the reactor.
242
- #
243
- # The general idea is that the thread pool can only work on a fixed
244
- # number of requests at the same time. If it is already processing that
245
- # number of requests then it is at capacity. If another Puma process has
246
- # spare capacity, then the request can be left on the socket so the other
247
- # worker can pick it up and process it.
248
- #
249
- # For example: if there are 5 threads, but only 4 working on
250
- # requests, this method will not wait and the `Puma::Server`
251
- # can pull a request right away.
252
- #
253
- # If there are 5 threads and all 5 of them are busy, then it will
254
- # pause here, and wait until the `not_full` condition variable is
255
- # signaled, usually this indicates that a request has been processed.
256
- #
257
- # It's important to note that even though the server might accept another
258
- # request, it might not be added to the `@todo` array right away.
259
- # For example if a slow client has only sent a header, but not a body
260
- # then the `@todo` array would stay the same size as the reactor works
261
- # to try to buffer the request. In that scenario the next call to this
262
- # method would not block and another request would be added into the reactor
263
- # by the server. This would continue until a fully buffered request
264
- # makes it through the reactor and can then be processed by the thread pool.
265
- def wait_until_not_full
266
- with_mutex do
267
- while true
268
- return if @shutdown
269
-
270
- # If we can still spin up new threads and there
271
- # is work queued that cannot be handled by waiting
272
- # threads, then accept more work until we would
273
- # spin up the max number of threads.
274
- return if busy_threads < @max
275
-
276
- @not_full.wait @mutex
277
- end
278
- end
279
- end
280
-
281
- # @version 5.0.0
282
- def wait_for_less_busy_worker(delay_s)
283
- return unless delay_s && delay_s > 0
284
-
285
- # Ruby MRI does GVL, this can result
286
- # in processing contention when multiple threads
287
- # (requests) are running concurrently
288
- return unless Puma.mri?
289
-
290
- with_mutex do
291
- return if @shutdown
292
-
293
- # do not delay, if we are not busy
294
- return unless busy_threads > 0
295
-
296
- # this will be signaled once a request finishes,
297
- # which can happen earlier than delay
298
- @not_full.wait @mutex, delay_s
299
- end
300
- end
301
-
302
279
  # If there are any free threads in the pool, tell one to go ahead
303
280
  # and exit. If +force+ is true, then a trim request is requested
304
281
  # even if all threads are being utilized.
@@ -358,12 +335,12 @@ module Puma
358
335
  end
359
336
 
360
337
  def auto_trim!(timeout=@auto_trim_time)
361
- @auto_trim = Automaton.new(self, timeout, "#{@name} threadpool trimmer", :trim)
338
+ @auto_trim = Automaton.new(self, timeout, "#{@name} tp trim", :trim)
362
339
  @auto_trim.start!
363
340
  end
364
341
 
365
342
  def auto_reap!(timeout=@reaping_time)
366
- @reaper = Automaton.new(self, timeout, "#{@name} threadpool reaper", :reap)
343
+ @reaper = Automaton.new(self, timeout, "#{@name} tp reap", :reap)
367
344
  @reaper.start!
368
345
  end
369
346
 
data/lib/puma/util.rb CHANGED
@@ -10,13 +10,6 @@ module Puma
10
10
  IO.pipe
11
11
  end
12
12
 
13
- # An instance method on Thread has been provided to address https://bugs.ruby-lang.org/issues/13632,
14
- # which currently effects some older versions of Ruby: 2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1
15
- # Additional context: https://github.com/puma/puma/pull/1345
16
- def purge_interrupt_queue
17
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
18
- end
19
-
20
13
  # Escapes and unescapes a URI escaped string with
21
14
  # +encoding+. +encoding+ will be the target encoding of the string
22
15
  # returned, and it defaults to UTF-8
data/lib/puma.rb CHANGED
@@ -75,4 +75,14 @@ module Puma
75
75
  def self.set_thread_name(name)
76
76
  Thread.current.name = "puma #{name}"
77
77
  end
78
+
79
+ # Shows deprecated warning for renamed methods.
80
+ # @example
81
+ # Puma.deprecate_method_change :on_booted, __callee__, __method__
82
+ #
83
+ def self.deprecate_method_change(method_old, method_caller, method_new)
84
+ if method_old == method_caller
85
+ warn "Use '#{method_new}', '#{method_caller}' is deprecated and will be removed in v8"
86
+ end
87
+ end
78
88
  end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This module is used as an 'include' file in code at bottom of file
4
3
  module Puma
4
+
5
+ # This module is used as an 'include' file in code at bottom of file. It loads
6
+ # into either `Rackup::Handler::Puma` or `Rack::Handler::Puma`.
7
+
5
8
  module RackHandler
6
9
  DEFAULT_OPTIONS = {
7
10
  :Verbose => false,
@@ -29,7 +32,7 @@ module Puma
29
32
 
30
33
  @events = options[:events] || ::Puma::Events.new
31
34
 
32
- conf = ::Puma::Configuration.new(options, default_options.merge({events: @events})) do |user_config, file_config, default_config|
35
+ conf = ::Puma::Configuration.new(options, default_options.merge({ events: @events })) do |user_config, file_config, default_config|
33
36
  if options.delete(:Verbose)
34
37
  begin
35
38
  require 'rack/commonlogger' # Rack 1.x
@@ -69,7 +72,7 @@ module Puma
69
72
 
70
73
  log_writer = options.delete(:Silent) ? ::Puma::LogWriter.strings : ::Puma::LogWriter.stdio
71
74
 
72
- launcher = ::Puma::Launcher.new(conf, :log_writer => log_writer, events: @events)
75
+ launcher = ::Puma::Launcher.new(conf, log_writer: log_writer, events: @events)
73
76
 
74
77
  yield launcher if block_given?
75
78
  begin
@@ -93,9 +96,9 @@ module Puma
93
96
  def set_host_port_to_config(host, port, config)
94
97
  config.clear_binds! if host || port
95
98
 
96
- if host && (host[0,1] == '.' || host[0,1] == '/')
99
+ if host&.start_with? '.', '/', '@'
97
100
  config.bind "unix://#{host}"
98
- elsif host && host =~ /^ssl:\/\//
101
+ elsif host&.start_with? 'ssl://'
99
102
  uri = URI.parse(host)
100
103
  uri.port ||= port || ::Puma::Configuration::DEFAULTS[:tcp_port]
101
104
  config.bind uri.to_s
@@ -115,7 +118,7 @@ module Puma
115
118
  end
116
119
 
117
120
  # rackup was removed in Rack 3, it is now a separate gem
118
- if Object.const_defined? :Rackup
121
+ if Object.const_defined?(:Rackup) && ::Rackup.const_defined?(:Handler)
119
122
  module Rackup
120
123
  module Handler
121
124
  module Puma
@@ -127,7 +130,7 @@ if Object.const_defined? :Rackup
127
130
  end
128
131
  end
129
132
  else
130
- do_register = Object.const_defined?(:Rack) && Rack.release < '3'
133
+ do_register = Object.const_defined?(:Rack) && ::Rack.release < '3'
131
134
  module Rack
132
135
  module Handler
133
136
  module Puma
data/tools/Dockerfile CHANGED
@@ -1,16 +1,26 @@
1
1
  # Use this Dockerfile to create minimal reproductions of issues
2
+ # Build (MRI): docker build -f tools/Dockerfile .
3
+ # Build (JRuby): docker build -f tools/Dockerfile --build-arg RUBY_IMAGE=jruby:9.4 .
2
4
 
3
- FROM ruby:3.2
5
+ ARG RUBY_IMAGE=ruby:latest
6
+ FROM ${RUBY_IMAGE}
4
7
 
5
- # throw errors if Gemfile has been modified since Gemfile.lock
6
- RUN bundle config --global frozen 1
8
+ # Set BUNDLE_FROZEN=false if you need to update Gemfile.lock during a build.
9
+ ARG BUNDLE_FROZEN=true
10
+
11
+ RUN apt-get update \
12
+ && apt-get install -y --no-install-recommends ragel procps git \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Only freeze Bundler and compile native extensions when using MRI.
16
+ RUN if [ "$(ruby -e 'print RUBY_ENGINE')" = "ruby" ] && [ "${BUNDLE_FROZEN}" = "true" ]; then bundle config --global frozen 1; fi
7
17
 
8
18
  WORKDIR /usr/src/app
9
19
 
10
20
  COPY . .
11
21
 
12
22
  RUN bundle install
13
- RUN bundle exec rake compile
23
+ RUN if [ "$(ruby -e 'print RUBY_ENGINE')" = "ruby" ]; then bundle exec rake compile; fi
14
24
 
15
25
  EXPOSE 9292
16
- CMD bundle exec bin/puma test/rackup/hello.ru
26
+ CMD ["bundle", "exec", "bin/puma", "test/rackup/hello.ru"]
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.4.1
4
+ version: 7.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Phoenix
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-01-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: nio4r
@@ -24,10 +23,11 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '2.0'
27
- description: Puma is a simple, fast, threaded, and highly parallel HTTP 1.1 server
28
- for Ruby/Rack applications. Puma is intended for use in both development and production
29
- environments. It's great for highly parallel Ruby implementations such as Rubinius
30
- and JRuby as well as as providing process worker support to support CRuby well.
26
+ description: |
27
+ Puma is a simple, fast, multi-threaded, and highly parallel HTTP 1.1 server
28
+ for Ruby/Rack applications. Puma is intended for use in both development and
29
+ production environments. It's great for highly parallel Ruby implementations such as
30
+ JRuby and TruffleRuby as well as as providing process worker support to support CRuby well.
31
31
  email:
32
32
  - evan@phx.io
33
33
  executables:
@@ -50,6 +50,7 @@ files:
50
50
  - docs/images/puma-connection-flow-no-reactor.png
51
51
  - docs/images/puma-connection-flow.png
52
52
  - docs/images/puma-general-arch.png
53
+ - docs/java_options.md
53
54
  - docs/jungle/README.md
54
55
  - docs/jungle/rc.d/README.md
55
56
  - docs/jungle/rc.d/puma
@@ -65,7 +66,6 @@ files:
65
66
  - docs/testing_benchmarks_local_files.md
66
67
  - docs/testing_test_rackup_ci_files.md
67
68
  - ext/puma_http11/PumaHttp11Service.java
68
- - ext/puma_http11/ext_help.h
69
69
  - ext/puma_http11/extconf.rb
70
70
  - ext/puma_http11/http11_parser.c
71
71
  - ext/puma_http11/http11_parser.h
@@ -86,6 +86,7 @@ files:
86
86
  - lib/puma/cluster.rb
87
87
  - lib/puma/cluster/worker.rb
88
88
  - lib/puma/cluster/worker_handle.rb
89
+ - lib/puma/cluster_accept_loop_delay.rb
89
90
  - lib/puma/commonlogger.rb
90
91
  - lib/puma/configuration.rb
91
92
  - lib/puma/const.rb
@@ -126,11 +127,11 @@ licenses:
126
127
  - BSD-3-Clause
127
128
  metadata:
128
129
  bug_tracker_uri: https://github.com/puma/puma/issues
129
- changelog_uri: https://github.com/puma/puma/blob/master/History.md
130
+ changelog_uri: https://github.com/puma/puma/blob/main/History.md
130
131
  homepage_uri: https://puma.io
131
132
  source_code_uri: https://github.com/puma/puma
132
133
  rubygems_mfa_required: 'true'
133
- post_install_message:
134
+ msys2_mingw_dependencies: openssl
134
135
  rdoc_options: []
135
136
  require_paths:
136
137
  - lib
@@ -138,16 +139,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
138
139
  requirements:
139
140
  - - ">="
140
141
  - !ruby/object:Gem::Version
141
- version: '2.4'
142
+ version: '3.0'
142
143
  required_rubygems_version: !ruby/object:Gem::Requirement
143
144
  requirements:
144
145
  - - ">="
145
146
  - !ruby/object:Gem::Version
146
147
  version: '0'
147
148
  requirements: []
148
- rubygems_version: 3.5.3
149
- signing_key:
149
+ rubygems_version: 4.0.6
150
150
  specification_version: 4
151
- summary: Puma is a simple, fast, threaded, and highly parallel HTTP 1.1 server for
152
- Ruby/Rack applications
151
+ summary: A Ruby/Rack web server built for parallelism.
153
152
  test_files: []
@@ -1,15 +0,0 @@
1
- #ifndef ext_help_h
2
- #define ext_help_h
3
-
4
- #define RAISE_NOT_NULL(T) if(T == NULL) rb_raise(rb_eArgError, "%s", "NULL found for " # T " when shouldn't be.");
5
- #define DATA_GET(from,type,data_type,name) TypedData_Get_Struct(from,type,data_type,name); RAISE_NOT_NULL(name);
6
- #define REQUIRE_TYPE(V, T) if(TYPE(V) != T) rb_raise(rb_eTypeError, "%s", "Wrong argument type for " # V " required " # T);
7
- #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
8
-
9
- #ifdef DEBUG
10
- #define TRACE() fprintf(stderr, "> %s:%d:%s\n", __FILE__, __LINE__, __FUNCTION__)
11
- #else
12
- #define TRACE()
13
- #endif
14
-
15
- #endif