puma 7.0.4 → 8.0.2

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +162 -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/binder.rb +2 -2
  26. data/lib/puma/cli.rb +1 -1
  27. data/lib/puma/client.rb +138 -102
  28. data/lib/puma/client_env.rb +171 -0
  29. data/lib/puma/cluster/worker.rb +10 -9
  30. data/lib/puma/cluster/worker_handle.rb +2 -2
  31. data/lib/puma/cluster.rb +12 -11
  32. data/lib/puma/cluster_accept_loop_delay.rb +17 -18
  33. data/lib/puma/configuration.rb +87 -17
  34. data/lib/puma/const.rb +4 -3
  35. data/lib/puma/control_cli.rb +1 -1
  36. data/lib/puma/detect.rb +11 -0
  37. data/lib/puma/dsl.rb +115 -18
  38. data/lib/puma/launcher/bundle_pruner.rb +2 -4
  39. data/lib/puma/launcher.rb +35 -30
  40. data/lib/puma/log_writer.rb +8 -2
  41. data/lib/puma/reactor.rb +3 -12
  42. data/lib/puma/{request.rb → response.rb} +25 -194
  43. data/lib/puma/runner.rb +1 -1
  44. data/lib/puma/server.rb +103 -64
  45. data/lib/puma/server_plugin_control.rb +32 -0
  46. data/lib/puma/single.rb +2 -2
  47. data/lib/puma/state_file.rb +3 -2
  48. data/lib/puma/thread_pool.rb +139 -24
  49. data/lib/rack/handler/puma.rb +1 -1
  50. data/tools/Dockerfile +13 -5
  51. metadata +16 -5
  52. data/ext/puma_http11/ext_help.h +0 -15
@@ -3,8 +3,13 @@
3
3
  require 'thread'
4
4
 
5
5
  require_relative 'io_buffer'
6
+ require_relative 'server_plugin_control'
6
7
 
7
8
  module Puma
9
+
10
+ # Add `Thread#puma_server` and `Thread#puma_server=`
11
+ Thread.attr_accessor(:puma_server)
12
+
8
13
  # Internal Docs for A simple thread pool management object.
9
14
  #
10
15
  # Each Puma "worker" has a thread pool to process requests.
@@ -20,6 +25,51 @@ module Puma
20
25
  class ForceShutdown < RuntimeError
21
26
  end
22
27
 
28
+ class ProcessorThread
29
+ attr_accessor :thread
30
+ attr_writer :marked_as_io_thread
31
+
32
+ def initialize(pool)
33
+ @pool = pool
34
+ @thread = nil
35
+ @marked_as_io_thread = false
36
+ end
37
+
38
+ def mark_as_io_thread!
39
+ unless @marked_as_io_thread
40
+ @marked_as_io_thread = true
41
+
42
+ # Immediately signal the pool that it can spawn a new thread
43
+ # if there's some work in the queue.
44
+ @pool.spawn_thread_if_needed
45
+ end
46
+ end
47
+
48
+ def marked_as_io_thread?
49
+ @marked_as_io_thread
50
+ end
51
+
52
+ def alive?
53
+ @thread&.alive?
54
+ end
55
+
56
+ def join(...)
57
+ @thread.join(...)
58
+ end
59
+
60
+ def kill(...)
61
+ @thread.kill(...)
62
+ end
63
+
64
+ def [](key)
65
+ @thread[key]
66
+ end
67
+
68
+ def raise(...)
69
+ @thread.raise(...)
70
+ end
71
+ end
72
+
23
73
  # How long, after raising the ForceShutdown of a thread during
24
74
  # forced shutdown mode, to wait for the thread to try and finish
25
75
  # up its work before leaving the thread to die on the vine.
@@ -33,7 +83,9 @@ module Puma
33
83
  # The block passed is the work that will be performed in each
34
84
  # thread.
35
85
  #
36
- def initialize(name, options = {}, &block)
86
+ def initialize(name, options = {}, server: nil, &block)
87
+ @server = server
88
+
37
89
  @not_empty = ConditionVariable.new
38
90
  @not_full = ConditionVariable.new
39
91
  @mutex = Mutex.new
@@ -46,10 +98,13 @@ module Puma
46
98
  @name = name
47
99
  @min = Integer(options[:min_threads])
48
100
  @max = Integer(options[:max_threads])
101
+ @max_io_threads = Integer(options[:max_io_threads] || 0)
102
+
49
103
  # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
50
104
  # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
51
105
  # makes stubbing constants difficult.
52
106
  @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
107
+ @shutdown_debug = options[:shutdown_debug]
53
108
  @block = block
54
109
  @out_of_band = options[:out_of_band]
55
110
  @out_of_band_running = false
@@ -64,7 +119,7 @@ module Puma
64
119
  @trim_requested = 0
65
120
  @out_of_band_pending = false
66
121
 
67
- @workers = []
122
+ @processors = []
68
123
 
69
124
  @auto_trim = nil
70
125
  @reaper = nil
@@ -81,6 +136,7 @@ module Puma
81
136
  end
82
137
 
83
138
  attr_reader :spawned, :trim_requested, :waiting
139
+ attr_accessor :min, :max
84
140
 
85
141
  # generate stats hash so as not to perform multiple locks
86
142
  # @return [Hash] hash containing stat info from ThreadPool
@@ -90,8 +146,9 @@ module Puma
90
146
  @backlog_max = 0
91
147
  { backlog: @todo.size,
92
148
  running: @spawned,
93
- pool_capacity: @waiting + (@max - @spawned),
149
+ pool_capacity: pool_capacity,
94
150
  busy_threads: @spawned - @waiting + @todo.size,
151
+ io_threads: @processors.count(&:marked_as_io_thread?),
95
152
  backlog_max: temp
96
153
  }
97
154
  end
@@ -115,7 +172,7 @@ module Puma
115
172
 
116
173
  # @!attribute [r] pool_capacity
117
174
  def pool_capacity
118
- waiting + (@max - spawned)
175
+ (waiting + (@max - spawned)).clamp(0, Float::INFINITY)
119
176
  end
120
177
 
121
178
  # @!attribute [r] busy_threads
@@ -132,8 +189,12 @@ module Puma
132
189
  @spawned += 1
133
190
 
134
191
  trigger_before_thread_start_hooks
135
- th = Thread.new(@spawned) do |spawned|
192
+ processor = ProcessorThread.new(self)
193
+ processor.thread = Thread.new(processor, @spawned) do |processor, spawned|
136
194
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
195
+ # Advertise server into the thread
196
+ Thread.current.puma_server = @server
197
+
137
198
  todo = @todo
138
199
  block = @block
139
200
  mutex = @mutex
@@ -144,11 +205,23 @@ module Puma
144
205
  work = nil
145
206
 
146
207
  mutex.synchronize do
208
+ if processor.marked_as_io_thread?
209
+ if @processors.count { |t| !t.marked_as_io_thread? } < @max
210
+ # We're not at max processor threads, so the io thread can rejoin the normal population.
211
+ processor.marked_as_io_thread = false
212
+ else
213
+ # We're already at max threads, so we exit the extra io thread.
214
+ @processors.delete(processor)
215
+ trigger_before_thread_exit_hooks
216
+ Thread.exit
217
+ end
218
+ end
219
+
147
220
  while todo.empty?
148
221
  if @trim_requested > 0
149
222
  @trim_requested -= 1
150
223
  @spawned -= 1
151
- @workers.delete th
224
+ @processors.delete(processor)
152
225
  not_full.signal
153
226
  trigger_before_thread_exit_hooks
154
227
  Thread.exit
@@ -170,16 +243,16 @@ module Puma
170
243
  end
171
244
 
172
245
  begin
173
- @out_of_band_pending = true if block.call(work)
246
+ @out_of_band_pending = true if block.call(processor, work)
174
247
  rescue Exception => e
175
248
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
176
249
  end
177
250
  end
178
251
  end
179
252
 
180
- @workers << th
253
+ @processors << processor
181
254
 
182
- th
255
+ processor
183
256
  end
184
257
 
185
258
  private :spawn_thread
@@ -189,7 +262,7 @@ module Puma
189
262
 
190
263
  @before_thread_start.each do |b|
191
264
  begin
192
- b[:block].call
265
+ b[:block].call(ServerPluginControl.new(@server))
193
266
  rescue Exception => e
194
267
  STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
195
268
  end
@@ -248,6 +321,16 @@ module Puma
248
321
  @mutex.synchronize(&block)
249
322
  end
250
323
 
324
+ # :nodoc:
325
+ #
326
+ # Must be called with @mutex held!
327
+ #
328
+ def can_spawn_processor?
329
+ io_processors_count = @processors.count(&:marked_as_io_thread?)
330
+ extra_io_processors_count = io_processors_count > @max_io_threads ? io_processors_count - @max_io_threads : 0
331
+ (@spawned - io_processors_count) < (@max - extra_io_processors_count)
332
+ end
333
+
251
334
  # Add +work+ to the todo list for a Thread to pickup and process.
252
335
  def <<(work)
253
336
  with_mutex do
@@ -259,12 +342,21 @@ module Puma
259
342
  t = @todo.size
260
343
  @backlog_max = t if t > @backlog_max
261
344
 
262
- if @waiting < @todo.size and @spawned < @max
345
+ if @waiting < @todo.size and can_spawn_processor?
263
346
  spawn_thread
264
347
  end
265
348
 
266
349
  @not_empty.signal
267
350
  end
351
+ self
352
+ end
353
+
354
+ def spawn_thread_if_needed # :nodoc:
355
+ with_mutex do
356
+ if @waiting < @todo.size and can_spawn_processor?
357
+ spawn_thread
358
+ end
359
+ end
268
360
  end
269
361
 
270
362
  # If there are any free threads in the pool, tell one to go ahead
@@ -285,16 +377,12 @@ module Puma
285
377
  # spawned counter so that new healthy threads could be created again.
286
378
  def reap
287
379
  with_mutex do
288
- dead_workers = @workers.reject(&:alive?)
380
+ @processors, dead_processors = @processors.partition(&:alive?)
289
381
 
290
- dead_workers.each do |worker|
291
- worker.kill
382
+ dead_processors.each do |processor|
383
+ processor.kill
292
384
  @spawned -= 1
293
385
  end
294
-
295
- @workers.delete_if do |w|
296
- dead_workers.include?(w)
297
- end
298
386
  end
299
387
  end
300
388
 
@@ -353,7 +441,7 @@ module Puma
353
441
  # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
354
442
  # threads. Finally, wait 1 second for remaining threads to exit.
355
443
  #
356
- def shutdown(timeout=-1)
444
+ def shutdown(timeout)
357
445
  threads = with_mutex do
358
446
  @shutdown = true
359
447
  @trim_requested = @spawned
@@ -362,8 +450,12 @@ module Puma
362
450
 
363
451
  @auto_trim&.stop
364
452
  @reaper&.stop
365
- # dup workers so that we join them all safely
366
- @workers.dup
453
+ # dup processors so that we join them all safely
454
+ @processors.dup
455
+ end
456
+
457
+ if @shutdown_debug == true
458
+ shutdown_debug("Shutdown initiated")
367
459
  end
368
460
 
369
461
  if timeout == -1
@@ -373,13 +465,16 @@ module Puma
373
465
  join = ->(inner_timeout) do
374
466
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
375
467
  threads.reject! do |t|
376
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
377
- t.join inner_timeout - elapsed
468
+ remaining = inner_timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
469
+ remaining > 0 && t.join(remaining)
378
470
  end
379
471
  end
380
472
 
381
473
  # Wait +timeout+ seconds for threads to finish.
382
474
  join.call(timeout)
475
+ if @shutdown_debug == :on_force && !threads.empty?
476
+ shutdown_debug("Shutdown timeout exceeded")
477
+ end
383
478
 
384
479
  # If threads are still running, raise ForceShutdown and wait to finish.
385
480
  @shutdown_mutex.synchronize do
@@ -389,6 +484,9 @@ module Puma
389
484
  end
390
485
  end
391
486
  join.call(@shutdown_grace_time)
487
+ if @shutdown_debug == :on_force && !threads.empty?
488
+ shutdown_debug("Shutdown grace timeout exceeded")
489
+ end
392
490
 
393
491
  # If threads are _still_ running, forcefully kill them and wait to finish.
394
492
  threads.each(&:kill)
@@ -396,7 +494,24 @@ module Puma
396
494
  end
397
495
 
398
496
  @spawned = 0
399
- @workers = []
497
+ @processors = []
498
+ end
499
+
500
+ private
501
+
502
+ def shutdown_debug(message)
503
+ pid = Process.pid
504
+ threads = Thread.list
505
+
506
+ $stdout.syswrite "#{pid}: #{message}\n"
507
+ $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
508
+
509
+ threads.each_with_index do |thread, index|
510
+ $stdout.syswrite "#{pid}: Thread #{index + 1}/#{threads.size}: #{thread.inspect}\n"
511
+ $stdout.syswrite "#{pid}: #{(thread.backtrace || []).join("\n#{pid}: ")}\n\n"
512
+ end
513
+
514
+ $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
400
515
  end
401
516
  end
402
517
  end
@@ -109,7 +109,7 @@ module Puma
109
109
  end
110
110
 
111
111
  if port
112
- host ||= ::Puma::Configuration::DEFAULTS[:tcp_host]
112
+ host ||= ::Puma::Configuration.default_tcp_host
113
113
  config.port port, host
114
114
  end
115
115
  end
data/tools/Dockerfile CHANGED
@@ -1,18 +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
- RUN apt-get update && apt-get install -y ragel
8
+ # Set BUNDLE_FROZEN=false if you need to update Gemfile.lock during a build.
9
+ ARG BUNDLE_FROZEN=true
6
10
 
7
- # throw errors if Gemfile has been modified since Gemfile.lock
8
- RUN bundle config --global frozen 1
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
9
17
 
10
18
  WORKDIR /usr/src/app
11
19
 
12
20
  COPY . .
13
21
 
14
22
  RUN bundle install
15
- RUN bundle exec rake compile
23
+ RUN if [ "$(ruby -e 'print RUBY_ENGINE')" = "ruby" ]; then bundle exec rake compile; fi
16
24
 
17
25
  EXPOSE 9292
18
26
  CMD ["bundle", "exec", "bin/puma", "test/rackup/hello.ru"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.4
4
+ version: 8.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Phoenix
@@ -43,13 +43,21 @@ files:
43
43
  - bin/puma
44
44
  - bin/puma-wild
45
45
  - bin/pumactl
46
+ - docs/5.0-Upgrade.md
47
+ - docs/6.0-Upgrade.md
48
+ - docs/7.0-Upgrade.md
49
+ - docs/8.0-Upgrade.md
46
50
  - docs/architecture.md
47
51
  - docs/compile_options.md
48
52
  - docs/deployment.md
49
53
  - docs/fork_worker.md
54
+ - docs/grpc.md
55
+ - docs/images/favicon.svg
50
56
  - docs/images/puma-connection-flow-no-reactor.png
51
57
  - docs/images/puma-connection-flow.png
52
58
  - docs/images/puma-general-arch.png
59
+ - docs/images/running-puma.svg
60
+ - docs/images/standard-logo.svg
53
61
  - docs/java_options.md
54
62
  - docs/jungle/README.md
55
63
  - docs/jungle/rc.d/README.md
@@ -66,7 +74,6 @@ files:
66
74
  - docs/testing_benchmarks_local_files.md
67
75
  - docs/testing_test_rackup_ci_files.md
68
76
  - ext/puma_http11/PumaHttp11Service.java
69
- - ext/puma_http11/ext_help.h
70
77
  - ext/puma_http11/extconf.rb
71
78
  - ext/puma_http11/http11_parser.c
72
79
  - ext/puma_http11/http11_parser.h
@@ -75,6 +82,7 @@ files:
75
82
  - ext/puma_http11/http11_parser_common.rl
76
83
  - ext/puma_http11/mini_ssl.c
77
84
  - ext/puma_http11/no_ssl/PumaHttp11Service.java
85
+ - ext/puma_http11/org/jruby/puma/EnvKey.java
78
86
  - ext/puma_http11/org/jruby/puma/Http11.java
79
87
  - ext/puma_http11/org/jruby/puma/Http11Parser.java
80
88
  - ext/puma_http11/org/jruby/puma/MiniSSL.java
@@ -84,6 +92,7 @@ files:
84
92
  - lib/puma/binder.rb
85
93
  - lib/puma/cli.rb
86
94
  - lib/puma/client.rb
95
+ - lib/puma/client_env.rb
87
96
  - lib/puma/cluster.rb
88
97
  - lib/puma/cluster/worker.rb
89
98
  - lib/puma/cluster/worker_handle.rb
@@ -112,10 +121,11 @@ files:
112
121
  - lib/puma/rack/urlmap.rb
113
122
  - lib/puma/rack_default.rb
114
123
  - lib/puma/reactor.rb
115
- - lib/puma/request.rb
124
+ - lib/puma/response.rb
116
125
  - lib/puma/runner.rb
117
126
  - lib/puma/sd_notify.rb
118
127
  - lib/puma/server.rb
128
+ - lib/puma/server_plugin_control.rb
119
129
  - lib/puma/single.rb
120
130
  - lib/puma/state_file.rb
121
131
  - lib/puma/thread_pool.rb
@@ -128,10 +138,11 @@ licenses:
128
138
  - BSD-3-Clause
129
139
  metadata:
130
140
  bug_tracker_uri: https://github.com/puma/puma/issues
131
- changelog_uri: https://github.com/puma/puma/blob/master/History.md
141
+ changelog_uri: https://github.com/puma/puma/blob/main/History.md
132
142
  homepage_uri: https://puma.io
133
143
  source_code_uri: https://github.com/puma/puma
134
144
  rubygems_mfa_required: 'true'
145
+ msys2_mingw_dependencies: openssl
135
146
  rdoc_options: []
136
147
  require_paths:
137
148
  - lib
@@ -146,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
157
  - !ruby/object:Gem::Version
147
158
  version: '0'
148
159
  requirements: []
149
- rubygems_version: 3.6.9
160
+ rubygems_version: 4.0.6
150
161
  specification_version: 4
151
162
  summary: A Ruby/Rack web server built for parallelism.
152
163
  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