puma 5.3.2 → 5.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +216 -11
  3. data/README.md +47 -6
  4. data/docs/architecture.md +49 -16
  5. data/docs/compile_options.md +4 -2
  6. data/docs/deployment.md +53 -67
  7. data/docs/plugins.md +15 -15
  8. data/docs/rails_dev_mode.md +2 -3
  9. data/docs/restart.md +6 -6
  10. data/docs/signals.md +11 -10
  11. data/docs/stats.md +8 -8
  12. data/docs/systemd.md +64 -67
  13. data/ext/puma_http11/extconf.rb +34 -6
  14. data/ext/puma_http11/http11_parser.c +23 -10
  15. data/ext/puma_http11/http11_parser_common.rl +1 -1
  16. data/ext/puma_http11/mini_ssl.c +90 -12
  17. data/ext/puma_http11/org/jruby/puma/Http11.java +2 -0
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +49 -47
  19. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +38 -55
  20. data/ext/puma_http11/puma_http11.c +1 -1
  21. data/lib/puma/app/status.rb +7 -4
  22. data/lib/puma/binder.rb +51 -6
  23. data/lib/puma/cli.rb +14 -4
  24. data/lib/puma/client.rb +143 -25
  25. data/lib/puma/cluster/worker.rb +8 -18
  26. data/lib/puma/cluster/worker_handle.rb +4 -0
  27. data/lib/puma/cluster.rb +30 -24
  28. data/lib/puma/configuration.rb +4 -1
  29. data/lib/puma/const.rb +17 -8
  30. data/lib/puma/control_cli.rb +19 -13
  31. data/lib/puma/detect.rb +8 -2
  32. data/lib/puma/dsl.rb +111 -13
  33. data/lib/puma/{json.rb → json_serialization.rb} +1 -1
  34. data/lib/puma/launcher.rb +15 -1
  35. data/lib/puma/minissl/context_builder.rb +8 -6
  36. data/lib/puma/minissl.rb +33 -27
  37. data/lib/puma/null_io.rb +5 -0
  38. data/lib/puma/plugin.rb +2 -2
  39. data/lib/puma/rack/builder.rb +1 -1
  40. data/lib/puma/request.rb +35 -13
  41. data/lib/puma/runner.rb +22 -8
  42. data/lib/puma/server.rb +37 -29
  43. data/lib/puma/state_file.rb +42 -7
  44. data/lib/puma/thread_pool.rb +7 -5
  45. data/lib/puma/util.rb +20 -4
  46. data/lib/puma.rb +6 -4
  47. data/lib/rack/version_restriction.rb +15 -0
  48. data/tools/Dockerfile +1 -1
  49. metadata +8 -7
data/lib/puma/server.rb CHANGED
@@ -14,6 +14,7 @@ require 'puma/io_buffer'
14
14
  require 'puma/request'
15
15
 
16
16
  require 'socket'
17
+ require 'io/wait'
17
18
  require 'forwardable'
18
19
 
19
20
  module Puma
@@ -38,6 +39,7 @@ module Puma
38
39
  attr_reader :events
39
40
  attr_reader :min_threads, :max_threads # for #stats
40
41
  attr_reader :requests_count # @version 5.0.0
42
+ attr_reader :log_writer # to help with backports
41
43
 
42
44
  # @todo the following may be deprecated in the future
43
45
  attr_reader :auto_trim_time, :early_hints, :first_data_timeout,
@@ -72,6 +74,7 @@ module Puma
72
74
  def initialize(app, events=Events.stdio, options={})
73
75
  @app = app
74
76
  @events = events
77
+ @log_writer = events
75
78
 
76
79
  @check, @notify = nil
77
80
  @status = :stop
@@ -145,7 +148,7 @@ module Puma
145
148
  begin
146
149
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if skt.kind_of? TCPSocket
147
150
  rescue IOError, SystemCallError
148
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
151
+ Puma::Util.purge_interrupt_queue
149
152
  end
150
153
  end
151
154
 
@@ -154,7 +157,7 @@ module Puma
154
157
  begin
155
158
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 0) if skt.kind_of? TCPSocket
156
159
  rescue IOError, SystemCallError
157
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
160
+ Puma::Util.purge_interrupt_queue
158
161
  end
159
162
  end
160
163
  else
@@ -175,7 +178,7 @@ module Puma
175
178
  begin
176
179
  tcp_info = skt.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
177
180
  rescue IOError, SystemCallError
178
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
181
+ Puma::Util.purge_interrupt_queue
179
182
  @precheck_closing = false
180
183
  false
181
184
  else
@@ -219,7 +222,7 @@ module Puma
219
222
  # up in the background to handle requests. Otherwise requests
220
223
  # are handled synchronously.
221
224
  #
222
- def run(background=true, thread_name: 'server')
225
+ def run(background=true, thread_name: 'srv')
223
226
  BasicSocket.do_not_reverse_lookup = true
224
227
 
225
228
  @events.fire :state, :booting
@@ -227,6 +230,7 @@ module Puma
227
230
  @status = :run
228
231
 
229
232
  @thread_pool = ThreadPool.new(
233
+ thread_name,
230
234
  @min_threads,
231
235
  @max_threads,
232
236
  ::Puma::IOBuffer,
@@ -313,14 +317,15 @@ module Puma
313
317
  queue_requests = @queue_requests
314
318
  drain = @options[:drain_on_shutdown] ? 0 : nil
315
319
 
316
- remote_addr_value = nil
317
- remote_addr_header = nil
318
-
319
- case @options[:remote_address]
320
+ addr_send_name, addr_value = case @options[:remote_address]
320
321
  when :value
321
- remote_addr_value = @options[:remote_address_value]
322
+ [:peerip=, @options[:remote_address_value]]
322
323
  when :header
323
- remote_addr_header = @options[:remote_address_header]
324
+ [:remote_addr_header=, @options[:remote_address_header]]
325
+ when :proxy_protocol
326
+ [:expect_proxy_proto=, @options[:remote_address_proxy_protocol]]
327
+ else
328
+ [nil, nil]
324
329
  end
325
330
 
326
331
  while @status == :run || (drain && shutting_down?)
@@ -340,17 +345,16 @@ module Puma
340
345
  next
341
346
  end
342
347
  drain += 1 if shutting_down?
343
- client = Client.new io, @binder.env(sock)
344
- client.listener = sock
345
- if remote_addr_value
346
- client.peerip = remote_addr_value
347
- elsif remote_addr_header
348
- client.remote_addr_header = remote_addr_header
349
- end
350
- pool << client
348
+ pool << Client.new(io, @binder.env(sock)).tap { |c|
349
+ c.listener = sock
350
+ c.send(addr_send_name, addr_value) if addr_value
351
+ }
351
352
  end
352
353
  end
353
- rescue Object => e
354
+ rescue IOError, Errno::EBADF
355
+ # In the case that any of the sockets are unexpectedly close.
356
+ raise
357
+ rescue StandardError => e
354
358
  @events.unknown_error e, nil, "Listen loop"
355
359
  end
356
360
  end
@@ -366,13 +370,14 @@ module Puma
366
370
  rescue Exception => e
367
371
  @events.unknown_error e, nil, "Exception handling servers"
368
372
  ensure
369
- begin
370
- @check.close unless @check.closed?
371
- rescue Errno::EBADF, RuntimeError
372
- # RuntimeError is Ruby 2.2 issue, can't modify frozen IOError
373
- # Errno::EBADF is infrequently raised
373
+ # RuntimeError is Ruby 2.2 issue, can't modify frozen IOError
374
+ # Errno::EBADF is infrequently raised
375
+ [@check, @notify].each do |io|
376
+ begin
377
+ io.close unless io.closed?
378
+ rescue Errno::EBADF, RuntimeError
379
+ end
374
380
  end
375
- @notify.close
376
381
  @notify = nil
377
382
  @check = nil
378
383
  end
@@ -396,7 +401,7 @@ module Puma
396
401
  return true
397
402
  end
398
403
 
399
- return false
404
+ false
400
405
  end
401
406
 
402
407
  # Given a connection on +client+, handle the incoming requests,
@@ -482,7 +487,7 @@ module Puma
482
487
  begin
483
488
  client.close if close_socket
484
489
  rescue IOError, SystemCallError
485
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
490
+ Puma::Util.purge_interrupt_queue
486
491
  # Already closed
487
492
  rescue StandardError => e
488
493
  @events.unknown_error e, nil, "Client"
@@ -512,6 +517,9 @@ module Puma
512
517
  when HttpParserError
513
518
  client.write_error(400)
514
519
  @events.parse_error e, client
520
+ when HttpParserError501
521
+ client.write_error(501)
522
+ @events.parse_error e, client
515
523
  else
516
524
  client.write_error(500)
517
525
  @events.unknown_error e, nil, "Read"
@@ -574,11 +582,11 @@ module Puma
574
582
  @notify << message
575
583
  rescue IOError, NoMethodError, Errno::EPIPE
576
584
  # The server, in another thread, is shutting down
577
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
585
+ Puma::Util.purge_interrupt_queue
578
586
  rescue RuntimeError => e
579
587
  # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
580
588
  if e.message.include?('IOError')
581
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
589
+ Puma::Util.purge_interrupt_queue
582
590
  else
583
591
  raise e
584
592
  end
@@ -1,15 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
-
5
3
  module Puma
4
+
5
+ # Puma::Launcher uses StateFile to write a yaml file for use with Puma::ControlCLI.
6
+ #
7
+ # In previous versions of Puma, YAML was used to read/write the state file.
8
+ # Since Puma is similar to Bundler/RubyGems in that it may load before one's app
9
+ # does, minimizing the dependencies that may be shared with the app is desired.
10
+ #
11
+ # At present, it only works with numeric and string values. It is still a valid
12
+ # yaml file, and the CI tests parse it with Psych.
13
+ #
6
14
  class StateFile
15
+
16
+ ALLOWED_FIELDS = %w!control_url control_auth_token pid running_from!
17
+
18
+ # @deprecated 6.0.0
19
+ FIELDS = ALLOWED_FIELDS
20
+
7
21
  def initialize
8
22
  @options = {}
9
23
  end
10
24
 
11
25
  def save(path, permission = nil)
12
- contents =YAML.dump @options
26
+ contents = "---\n".dup
27
+ @options.each do |k,v|
28
+ next unless ALLOWED_FIELDS.include? k
29
+ case v
30
+ when Numeric
31
+ contents << "#{k}: #{v}\n"
32
+ when String
33
+ next if v.strip.empty?
34
+ contents << (k == 'running_from' || v.to_s.include?(' ') ?
35
+ "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
36
+ end
37
+ end
13
38
  if permission
14
39
  File.write path, contents, mode: 'wb:UTF-8'
15
40
  else
@@ -18,12 +43,22 @@ module Puma
18
43
  end
19
44
 
20
45
  def load(path)
21
- @options = YAML.load File.read(path)
46
+ File.read(path).lines.each do |line|
47
+ next if line.start_with? '#'
48
+ k,v = line.split ':', 2
49
+ next unless v && ALLOWED_FIELDS.include?(k)
50
+ v = v.strip
51
+ @options[k] =
52
+ case v
53
+ when '' then nil
54
+ when /\A\d+\z/ then v.to_i
55
+ when /\A\d+\.\d+\z/ then v.to_f
56
+ else v.gsub(/\A"|"\z/, '')
57
+ end
58
+ end
22
59
  end
23
60
 
24
- FIELDS = %w!control_url control_auth_token pid running_from!
25
-
26
- FIELDS.each do |f|
61
+ ALLOWED_FIELDS.each do |f|
27
62
  define_method f do
28
63
  @options[f]
29
64
  end
@@ -29,7 +29,7 @@ module Puma
29
29
  # The block passed is the work that will be performed in each
30
30
  # thread.
31
31
  #
32
- def initialize(min, max, *extra, &block)
32
+ def initialize(name, min, max, *extra, &block)
33
33
  @not_empty = ConditionVariable.new
34
34
  @not_full = ConditionVariable.new
35
35
  @mutex = Mutex.new
@@ -39,6 +39,7 @@ module Puma
39
39
  @spawned = 0
40
40
  @waiting = 0
41
41
 
42
+ @name = name
42
43
  @min = Integer(min)
43
44
  @max = Integer(max)
44
45
  @block = block
@@ -71,7 +72,7 @@ module Puma
71
72
  attr_accessor :out_of_band_hook # @version 5.0.0
72
73
 
73
74
  def self.clean_thread_locals
74
- Thread.current.keys.each do |key| # rubocop: disable Performance/HashEachMethods
75
+ Thread.current.keys.each do |key| # rubocop: disable Style/HashEachMethods
75
76
  Thread.current[key] = nil unless key == :__recursive_key__
76
77
  end
77
78
  end
@@ -101,7 +102,7 @@ module Puma
101
102
  @spawned += 1
102
103
 
103
104
  th = Thread.new(@spawned) do |spawned|
104
- Puma.set_thread_name 'threadpool %03i' % spawned
105
+ Puma.set_thread_name '%s tp %03i' % [@name, spawned]
105
106
  todo = @todo
106
107
  block = @block
107
108
  mutex = @mutex
@@ -119,6 +120,7 @@ module Puma
119
120
  @trim_requested -= 1
120
121
  @spawned -= 1
121
122
  @workers.delete th
123
+ not_full.signal
122
124
  Thread.exit
123
125
  end
124
126
 
@@ -318,12 +320,12 @@ module Puma
318
320
  end
319
321
 
320
322
  def auto_trim!(timeout=30)
321
- @auto_trim = Automaton.new(self, timeout, "threadpool trimmer", :trim)
323
+ @auto_trim = Automaton.new(self, timeout, "#{@name} threadpool trimmer", :trim)
322
324
  @auto_trim.start!
323
325
  end
324
326
 
325
327
  def auto_reap!(timeout=5)
326
- @reaper = Automaton.new(self, timeout, "threadpool reaper", :reap)
328
+ @reaper = Automaton.new(self, timeout, "#{@name} threadpool reaper", :reap)
327
329
  @reaper.start!
328
330
  end
329
331
 
data/lib/puma/util.rb CHANGED
@@ -10,18 +10,34 @@ module Puma
10
10
  IO.pipe
11
11
  end
12
12
 
13
- # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
14
- # target encoding of the string returned, and it defaults to UTF-8
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
+ # Escapes and unescapes a URI escaped string with
21
+ # +encoding+. +encoding+ will be the target encoding of the string
22
+ # returned, and it defaults to UTF-8
15
23
  if defined?(::Encoding)
24
+ def escape(s, encoding = Encoding::UTF_8)
25
+ URI.encode_www_form_component(s, encoding)
26
+ end
27
+
16
28
  def unescape(s, encoding = Encoding::UTF_8)
17
29
  URI.decode_www_form_component(s, encoding)
18
30
  end
19
31
  else
32
+ def escape(s, encoding = nil)
33
+ URI.encode_www_form_component(s, encoding)
34
+ end
35
+
20
36
  def unescape(s, encoding = nil)
21
37
  URI.decode_www_form_component(s, encoding)
22
38
  end
23
39
  end
24
- module_function :unescape
40
+ module_function :unescape, :escape
25
41
 
26
42
  # @version 5.0.0
27
43
  def nakayoshi_gc(events)
@@ -61,7 +77,7 @@ module Puma
61
77
  end
62
78
  end
63
79
 
64
- return params
80
+ params
65
81
  end
66
82
 
67
83
  # A case-insensitive Hash that preserves the original case of a
data/lib/puma.rb CHANGED
@@ -10,9 +10,11 @@ require 'stringio'
10
10
 
11
11
  require 'thread'
12
12
 
13
+ # extension files should not be loaded with `require_relative`
13
14
  require 'puma/puma_http11'
14
- require 'puma/detect'
15
- require 'puma/json'
15
+ require_relative 'puma/detect'
16
+ require_relative 'puma/json_serialization'
17
+ require_relative 'rack/version_restriction'
16
18
 
17
19
  module Puma
18
20
  autoload :Const, 'puma/const'
@@ -23,7 +25,7 @@ module Puma
23
25
  # not in minissl.rb
24
26
  HAS_SSL = const_defined?(:MiniSSL, false) && MiniSSL.const_defined?(:Engine, false)
25
27
 
26
- HAS_UNIX_SOCKET = Object.const_defined? :UNIXSocket
28
+ HAS_UNIX_SOCKET = Object.const_defined?(:UNIXSocket) && !IS_WINDOWS
27
29
 
28
30
  if HAS_SSL
29
31
  require 'puma/minissl'
@@ -60,7 +62,7 @@ module Puma
60
62
 
61
63
  # @!attribute [rw] stats_object
62
64
  def self.stats
63
- Puma::JSON.generate @get_stats.stats
65
+ Puma::JSONSerialization.generate @get_stats.stats
64
66
  end
65
67
 
66
68
  # @!attribute [r] stats_hash
@@ -0,0 +1,15 @@
1
+ begin
2
+ begin
3
+ # rack/version exists in Rack 2.2.0 and later, compatible with Ruby 2.3 and later
4
+ # we prefer to not load Rack
5
+ require 'rack/version'
6
+ rescue LoadError
7
+ require 'rack'
8
+ end
9
+
10
+ # Rack.release is needed for Rack v1, Rack::RELEASE was added in v2
11
+ if Gem::Version.new(Rack.release) >= Gem::Version.new("3.0.0")
12
+ raise StandardError.new "Puma 5 is not compatible with Rack 3, please upgrade to Puma 6 or higher."
13
+ end
14
+ rescue LoadError
15
+ end
data/tools/Dockerfile CHANGED
@@ -1,6 +1,6 @@
1
1
  # Use this Dockerfile to create minimal reproductions of issues
2
2
 
3
- FROM ruby:2.6
3
+ FROM ruby:3.1
4
4
 
5
5
  # throw errors if Gemfile has been modified since Gemfile.lock
6
6
  RUN bundle config --global frozen 1
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.2
4
+ version: 5.6.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Phoenix
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-21 00:00:00.000000000 Z
11
+ date: 2024-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nio4r
@@ -24,9 +24,9 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
- description: Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server
27
+ description: Puma is a simple, fast, threaded, and highly parallel HTTP 1.1 server
28
28
  for Ruby/Rack applications. Puma is intended for use in both development and production
29
- environments. It's great for highly concurrent Ruby implementations such as Rubinius
29
+ environments. It's great for highly parallel Ruby implementations such as Rubinius
30
30
  and JRuby as well as as providing process worker support to support CRuby well.
31
31
  email:
32
32
  - evan@phx.io
@@ -94,7 +94,7 @@ files:
94
94
  - lib/puma/events.rb
95
95
  - lib/puma/io_buffer.rb
96
96
  - lib/puma/jruby_restart.rb
97
- - lib/puma/json.rb
97
+ - lib/puma/json_serialization.rb
98
98
  - lib/puma/launcher.rb
99
99
  - lib/puma/minissl.rb
100
100
  - lib/puma/minissl/context_builder.rb
@@ -115,6 +115,7 @@ files:
115
115
  - lib/puma/thread_pool.rb
116
116
  - lib/puma/util.rb
117
117
  - lib/rack/handler/puma.rb
118
+ - lib/rack/version_restriction.rb
118
119
  - tools/Dockerfile
119
120
  - tools/trickletest.rb
120
121
  homepage: https://puma.io
@@ -140,9 +141,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
141
  - !ruby/object:Gem::Version
141
142
  version: '0'
142
143
  requirements: []
143
- rubygems_version: 3.2.3
144
+ rubygems_version: 3.5.16
144
145
  signing_key:
145
146
  specification_version: 4
146
- summary: Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for
147
+ summary: Puma is a simple, fast, threaded, and highly parallel HTTP 1.1 server for
147
148
  Ruby/Rack applications
148
149
  test_files: []