puma 5.6.8 → 6.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +332 -16
  3. data/README.md +79 -29
  4. data/bin/puma-wild +1 -1
  5. data/docs/compile_options.md +34 -0
  6. data/docs/fork_worker.md +1 -3
  7. data/docs/kubernetes.md +12 -0
  8. data/docs/nginx.md +1 -1
  9. data/docs/restart.md +1 -0
  10. data/docs/systemd.md +3 -6
  11. data/docs/testing_benchmarks_local_files.md +150 -0
  12. data/docs/testing_test_rackup_ci_files.md +36 -0
  13. data/ext/puma_http11/extconf.rb +16 -9
  14. data/ext/puma_http11/http11_parser.c +1 -1
  15. data/ext/puma_http11/http11_parser.h +1 -1
  16. data/ext/puma_http11/http11_parser.java.rl +2 -2
  17. data/ext/puma_http11/http11_parser.rl +2 -2
  18. data/ext/puma_http11/http11_parser_common.rl +2 -2
  19. data/ext/puma_http11/mini_ssl.c +127 -19
  20. data/ext/puma_http11/org/jruby/puma/Http11.java +5 -3
  21. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  22. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
  23. data/ext/puma_http11/puma_http11.c +17 -9
  24. data/lib/puma/app/status.rb +4 -4
  25. data/lib/puma/binder.rb +50 -53
  26. data/lib/puma/cli.rb +16 -18
  27. data/lib/puma/client.rb +59 -19
  28. data/lib/puma/cluster/worker.rb +18 -11
  29. data/lib/puma/cluster/worker_handle.rb +4 -1
  30. data/lib/puma/cluster.rb +102 -40
  31. data/lib/puma/commonlogger.rb +21 -14
  32. data/lib/puma/configuration.rb +77 -59
  33. data/lib/puma/const.rb +137 -92
  34. data/lib/puma/control_cli.rb +15 -11
  35. data/lib/puma/detect.rb +7 -4
  36. data/lib/puma/dsl.rb +250 -56
  37. data/lib/puma/error_logger.rb +18 -9
  38. data/lib/puma/events.rb +6 -126
  39. data/lib/puma/io_buffer.rb +39 -4
  40. data/lib/puma/jruby_restart.rb +2 -1
  41. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  42. data/lib/puma/launcher.rb +102 -175
  43. data/lib/puma/log_writer.rb +147 -0
  44. data/lib/puma/minissl/context_builder.rb +26 -12
  45. data/lib/puma/minissl.rb +104 -11
  46. data/lib/puma/null_io.rb +16 -2
  47. data/lib/puma/plugin/systemd.rb +90 -0
  48. data/lib/puma/plugin/tmp_restart.rb +1 -1
  49. data/lib/puma/rack/builder.rb +6 -6
  50. data/lib/puma/rack/urlmap.rb +1 -1
  51. data/lib/puma/rack_default.rb +19 -4
  52. data/lib/puma/reactor.rb +19 -10
  53. data/lib/puma/request.rb +380 -172
  54. data/lib/puma/runner.rb +56 -20
  55. data/lib/puma/sd_notify.rb +149 -0
  56. data/lib/puma/server.rb +137 -89
  57. data/lib/puma/single.rb +13 -11
  58. data/lib/puma/state_file.rb +3 -6
  59. data/lib/puma/thread_pool.rb +57 -19
  60. data/lib/puma/util.rb +0 -11
  61. data/lib/puma.rb +9 -10
  62. data/lib/rack/handler/puma.rb +113 -86
  63. data/tools/Dockerfile +2 -2
  64. metadata +11 -7
  65. data/lib/puma/queue_close.rb +0 -26
  66. data/lib/puma/systemd.rb +0 -46
  67. data/lib/rack/version_restriction.rb +0 -15
data/lib/puma/minissl.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  begin
4
- require 'io/wait'
4
+ require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
5
5
  rescue LoadError
6
6
  end
7
7
 
8
+ require 'open3'
8
9
  # need for Puma::MiniSSL::OPENSSL constants used in `HAS_TLS1_3`
10
+ # use require, see https://github.com/puma/puma/pull/2381
9
11
  require 'puma/puma_http11'
10
12
 
11
13
  module Puma
@@ -13,15 +15,16 @@ module Puma
13
15
  # Define constant at runtime, as it's easy to determine at built time,
14
16
  # but Puma could (it shouldn't) be loaded with an older OpenSSL version
15
17
  # @version 5.0.0
16
- HAS_TLS1_3 = !IS_JRUBY &&
17
- (OPENSSL_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) != -1 &&
18
- (OPENSSL_LIBRARY_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) !=-1
18
+ HAS_TLS1_3 = IS_JRUBY ||
19
+ ((OPENSSL_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) != -1 &&
20
+ (OPENSSL_LIBRARY_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) !=-1)
19
21
 
20
22
  class Socket
21
23
  def initialize(socket, engine)
22
24
  @socket = socket
23
25
  @engine = engine
24
26
  @peercert = nil
27
+ @reuse = nil
25
28
  end
26
29
 
27
30
  # @!attribute [r] to_io
@@ -50,7 +53,7 @@ module Puma
50
53
  # is made with TLSv1.3 as an available protocol
51
54
  # @version 5.0.0
52
55
  def bad_tlsv1_3?
53
- HAS_TLS1_3 && @engine.ssl_vers_st == ['TLSv1.3', 'SSLERR']
56
+ HAS_TLS1_3 && ssl_version_state == ['TLSv1.3', 'SSLERR']
54
57
  end
55
58
  private :bad_tlsv1_3?
56
59
 
@@ -123,7 +126,7 @@ module Puma
123
126
  while true
124
127
  wrote = @engine.write data
125
128
 
126
- enc_wr = ''.dup
129
+ enc_wr = +''
127
130
  while (enc = @engine.extract)
128
131
  enc_wr << enc
129
132
  end
@@ -181,6 +184,11 @@ module Puma
181
184
  @socket.peeraddr
182
185
  end
183
186
 
187
+ # OpenSSL is loaded in `MiniSSL::ContextBuilder` when
188
+ # `MiniSSL::Context#verify_mode` is not `VERIFY_NONE`.
189
+ # When `VERIFY_NONE`, `MiniSSL::Engine#peercert` is nil, regardless of
190
+ # whether the client sends a cert.
191
+ # @return [OpenSSL::X509::Certificate, nil]
184
192
  # @!attribute [r] peercert
185
193
  def peercert
186
194
  return @peercert if @peercert
@@ -195,10 +203,6 @@ module Puma
195
203
  if IS_JRUBY
196
204
  OPENSSL_NO_SSL3 = false
197
205
  OPENSSL_NO_TLS1 = false
198
-
199
- class SSLError < StandardError
200
- # Define this for jruby even though it isn't used.
201
- end
202
206
  end
203
207
 
204
208
  class Context
@@ -212,6 +216,9 @@ module Puma
212
216
  @cert = nil
213
217
  @key_pem = nil
214
218
  @cert_pem = nil
219
+ @reuse = nil
220
+ @reuse_cache_size = nil
221
+ @reuse_timeout = nil
215
222
  end
216
223
 
217
224
  def check_file(file, desc)
@@ -222,21 +229,61 @@ module Puma
222
229
  if IS_JRUBY
223
230
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
224
231
  attr_reader :keystore
232
+ attr_reader :keystore_type
225
233
  attr_accessor :keystore_pass
226
- attr_accessor :ssl_cipher_list
234
+ attr_reader :truststore
235
+ attr_reader :truststore_type
236
+ attr_accessor :truststore_pass
237
+ attr_reader :cipher_suites
238
+ attr_reader :protocols
227
239
 
228
240
  def keystore=(keystore)
229
241
  check_file keystore, 'Keystore'
230
242
  @keystore = keystore
231
243
  end
232
244
 
245
+ def truststore=(truststore)
246
+ # NOTE: historically truststore was assumed the same as keystore, this is kept for backwards
247
+ # compatibility, to rely on JVM's trust defaults we allow setting `truststore = :default`
248
+ unless truststore.eql?(:default)
249
+ raise ArgumentError, "No such truststore file '#{truststore}'" unless File.exist?(truststore)
250
+ end
251
+ @truststore = truststore
252
+ end
253
+
254
+ def keystore_type=(type)
255
+ raise ArgumentError, "Invalid keystore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
256
+ @keystore_type = type
257
+ end
258
+
259
+ def truststore_type=(type)
260
+ raise ArgumentError, "Invalid truststore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
261
+ @truststore_type = type
262
+ end
263
+
264
+ def cipher_suites=(list)
265
+ list = list.split(',').map(&:strip) if list.is_a?(String)
266
+ @cipher_suites = list
267
+ end
268
+
269
+ # aliases for backwards compatibility
270
+ alias_method :ssl_cipher_list, :cipher_suites
271
+ alias_method :ssl_cipher_list=, :cipher_suites=
272
+
273
+ def protocols=(list)
274
+ list = list.split(',').map(&:strip) if list.is_a?(String)
275
+ @protocols = list
276
+ end
277
+
233
278
  def check
234
279
  raise "Keystore not configured" unless @keystore
280
+ # @truststore defaults to @keystore due backwards compatibility
235
281
  end
236
282
 
237
283
  else
238
284
  # non-jruby Context properties
239
285
  attr_reader :key
286
+ attr_reader :key_password_command
240
287
  attr_reader :cert
241
288
  attr_reader :ca
242
289
  attr_reader :cert_pem
@@ -244,11 +291,17 @@ module Puma
244
291
  attr_accessor :ssl_cipher_filter
245
292
  attr_accessor :verification_flags
246
293
 
294
+ attr_reader :reuse, :reuse_cache_size, :reuse_timeout
295
+
247
296
  def key=(key)
248
297
  check_file key, 'Key'
249
298
  @key = key
250
299
  end
251
300
 
301
+ def key_password_command=(key_password_command)
302
+ @key_password_command = key_password_command
303
+ end
304
+
252
305
  def cert=(cert)
253
306
  check_file cert, 'Cert'
254
307
  @cert = cert
@@ -273,6 +326,46 @@ module Puma
273
326
  raise "Key not configured" if @key.nil? && @key_pem.nil?
274
327
  raise "Cert not configured" if @cert.nil? && @cert_pem.nil?
275
328
  end
329
+
330
+ # Executes the command to return the password needed to decrypt the key.
331
+ def key_password
332
+ raise "Key password command not configured" if @key_password_command.nil?
333
+
334
+ stdout_str, stderr_str, status = Open3.capture3(@key_password_command)
335
+
336
+ return stdout_str.chomp if status.success?
337
+
338
+ raise "Key password failed with code #{status.exitstatus}: #{stderr_str}"
339
+ end
340
+
341
+ # Controls session reuse. Allowed values are as follows:
342
+ # * 'off' - matches the behavior of Puma 5.6 and earlier. This is included
343
+ # in case reuse 'on' is made the default in future Puma versions.
344
+ # * 'dflt' - sets session reuse on, with OpenSSL default cache size of
345
+ # 20k and default timeout of 300 seconds.
346
+ # * 's,t' - where s and t are integer strings, for size and timeout.
347
+ # * 's' - where s is an integer strings for size.
348
+ # * ',t' - where t is an integer strings for timeout.
349
+ #
350
+ def reuse=(reuse_str)
351
+ case reuse_str
352
+ when 'off'
353
+ @reuse = nil
354
+ when 'dflt'
355
+ @reuse = true
356
+ when /\A\d+\z/
357
+ @reuse = true
358
+ @reuse_cache_size = reuse_str.to_i
359
+ when /\A\d+,\d+\z/
360
+ @reuse = true
361
+ size, time = reuse_str.split ','
362
+ @reuse_cache_size = size.to_i
363
+ @reuse_timeout = time.to_i
364
+ when /\A,\d+\z/
365
+ @reuse = true
366
+ @reuse_timeout = reuse_str.delete(',').to_i
367
+ end
368
+ end
276
369
  end
277
370
 
278
371
  # disables TLSv1
data/lib/puma/null_io.rb CHANGED
@@ -18,8 +18,22 @@ module Puma
18
18
 
19
19
  # Mimics IO#read with no data.
20
20
  #
21
- def read(count = nil, _buffer = nil)
22
- count && count > 0 ? nil : ""
21
+ def read(length = nil, buffer = nil)
22
+ if length.to_i < 0
23
+ raise ArgumentError, "(negative length #{length} given)"
24
+ end
25
+
26
+ buffer = if buffer.nil?
27
+ "".b
28
+ else
29
+ String.try_convert(buffer) or raise TypeError, "no implicit conversion of #{buffer.class} into String"
30
+ end
31
+ buffer.clear
32
+ if length.to_i > 0
33
+ nil
34
+ else
35
+ buffer
36
+ end
23
37
  end
24
38
 
25
39
  def rewind
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../plugin'
4
+
5
+ # Puma's systemd integration allows Puma to inform systemd:
6
+ # 1. when it has successfully started
7
+ # 2. when it is starting shutdown
8
+ # 3. periodically for a liveness check with a watchdog thread
9
+ # 4. periodically set the status
10
+ Puma::Plugin.create do
11
+ def start(launcher)
12
+ require_relative '../sd_notify'
13
+
14
+ launcher.log_writer.log "* Enabling systemd notification integration"
15
+
16
+ # hook_events
17
+ launcher.events.on_booted { Puma::SdNotify.ready }
18
+ launcher.events.on_stopped { Puma::SdNotify.stopping }
19
+ launcher.events.on_restart { Puma::SdNotify.reloading }
20
+
21
+ # start watchdog
22
+ if Puma::SdNotify.watchdog?
23
+ ping_f = watchdog_sleep_time
24
+
25
+ in_background do
26
+ launcher.log_writer.log "Pinging systemd watchdog every #{ping_f.round(1)} sec"
27
+ loop do
28
+ sleep ping_f
29
+ Puma::SdNotify.watchdog
30
+ end
31
+ end
32
+ end
33
+
34
+ # start status loop
35
+ instance = self
36
+ sleep_time = 1.0
37
+ in_background do
38
+ launcher.log_writer.log "Sending status to systemd every #{sleep_time.round(1)} sec"
39
+
40
+ loop do
41
+ sleep sleep_time
42
+ # TODO: error handling?
43
+ Puma::SdNotify.status(instance.status)
44
+ end
45
+ end
46
+ end
47
+
48
+ def status
49
+ if clustered?
50
+ messages = stats[:worker_status].map do |worker|
51
+ common_message(worker[:last_status])
52
+ end.join(',')
53
+
54
+ "Puma #{Puma::Const::VERSION}: cluster: #{booted_workers}/#{workers}, worker_status: [#{messages}]"
55
+ else
56
+ "Puma #{Puma::Const::VERSION}: worker: #{common_message(stats)}"
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def watchdog_sleep_time
63
+ usec = Integer(ENV["WATCHDOG_USEC"])
64
+
65
+ sec_f = usec / 1_000_000.0
66
+ # "It is recommended that a daemon sends a keep-alive notification message
67
+ # to the service manager every half of the time returned here."
68
+ sec_f / 2
69
+ end
70
+
71
+ def stats
72
+ Puma.stats_hash
73
+ end
74
+
75
+ def clustered?
76
+ stats.has_key?(:workers)
77
+ end
78
+
79
+ def workers
80
+ stats.fetch(:workers, 1)
81
+ end
82
+
83
+ def booted_workers
84
+ stats.fetch(:booted_workers, 1)
85
+ end
86
+
87
+ def common_message(stats)
88
+ "{ #{stats[:running]}/#{stats[:max_threads]} threads, #{stats[:pool_capacity]} available, #{stats[:backlog]} backlog }"
89
+ end
90
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/plugin'
3
+ require_relative '../plugin'
4
4
 
5
5
  Puma::Plugin.create do
6
6
  def start(launcher)
@@ -102,13 +102,13 @@ module Puma::Rack
102
102
  begin
103
103
  info = []
104
104
  server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
105
- if server && server.respond_to?(:valid_options)
105
+ if server&.respond_to?(:valid_options)
106
106
  info << ""
107
107
  info << "Server-specific options for #{server.name}:"
108
108
 
109
109
  has_options = false
110
110
  server.valid_options.each do |name, description|
111
- next if name.to_s =~ /^(Host|Port)[^a-zA-Z]/ # ignore handler's host and port options, we do our own.
111
+ next if /^(Host|Port)[^a-zA-Z]/.match? name.to_s # ignore handler's host and port options, we do our own.
112
112
 
113
113
  info << " -O %-21s %s" % [name, description]
114
114
  has_options = true
@@ -173,7 +173,7 @@ module Puma::Rack
173
173
  TOPLEVEL_BINDING, file, 0
174
174
  end
175
175
 
176
- def initialize(default_app = nil,&block)
176
+ def initialize(default_app = nil, &block)
177
177
  @use, @map, @run, @warmup = [], nil, default_app, nil
178
178
 
179
179
  # Conditionally load rack now, so that any rack middlewares,
@@ -183,7 +183,7 @@ module Puma::Rack
183
183
  rescue LoadError
184
184
  end
185
185
 
186
- instance_eval(&block) if block_given?
186
+ instance_eval(&block) if block
187
187
  end
188
188
 
189
189
  def self.app(default_app = nil, &block)
@@ -276,7 +276,7 @@ module Puma::Rack
276
276
  app = @map ? generate_map(@run, @map) : @run
277
277
  fail "missing run or map statement" unless app
278
278
  app = @use.reverse.inject(app) { |a,e| e[a] }
279
- @warmup.call(app) if @warmup
279
+ @warmup&.call app
280
280
  app
281
281
  end
282
282
 
@@ -287,7 +287,7 @@ module Puma::Rack
287
287
  private
288
288
 
289
289
  def generate_map(default_app, mapping)
290
- require 'puma/rack/urlmap'
290
+ require_relative 'urlmap'
291
291
 
292
292
  mapped = default_app ? {'/' => default_app} : {}
293
293
  mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app }
@@ -34,7 +34,7 @@ module Puma::Rack
34
34
  end
35
35
 
36
36
  location = location.chomp('/')
37
- match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
37
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
38
38
 
39
39
  [host, location, match, app]
40
40
  }.sort_by do |(host, location, _, _)|
@@ -1,9 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/handler/puma'
3
+ require_relative '../rack/handler/puma'
4
4
 
5
- module Rack::Handler
6
- def self.default(options = {})
7
- Rack::Handler::Puma
5
+ # rackup was removed in Rack 3, it is now a separate gem
6
+ if Object.const_defined? :Rackup
7
+ module Rackup
8
+ module Handler
9
+ def self.default(options = {})
10
+ ::Rackup::Handler::Puma
11
+ end
12
+ end
8
13
  end
14
+ elsif Object.const_defined?(:Rack) && Rack.release < '3'
15
+ module Rack
16
+ module Handler
17
+ def self.default(options = {})
18
+ ::Rack::Handler::Puma
19
+ end
20
+ end
21
+ end
22
+ else
23
+ raise "Rack 3 must be used with the Rackup gem"
9
24
  end
data/lib/puma/reactor.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/queue_close' unless ::Queue.instance_methods.include? :close
4
-
5
3
  module Puma
6
4
  class UnsupportedBackend < StandardError; end
7
5
 
@@ -22,10 +20,12 @@ module Puma
22
20
  # its timeout elapses, or when the Reactor shuts down.
23
21
  def initialize(backend, &block)
24
22
  require 'nio'
25
- unless backend == :auto || NIO::Selector.backends.include?(backend)
26
- raise "unsupported IO selector backend: #{backend} (available backends: #{NIO::Selector.backends.join(', ')})"
23
+ valid_backends = [:auto, *::NIO::Selector.backends]
24
+ unless valid_backends.include?(backend)
25
+ raise ArgumentError.new("unsupported IO selector backend: #{backend} (available backends: #{valid_backends.join(', ')})")
27
26
  end
28
- @selector = backend == :auto ? NIO::Selector.new : NIO::Selector.new(backend)
27
+
28
+ @selector = ::NIO::Selector.new(NIO::Selector.backends.delete(backend))
29
29
  @input = Queue.new
30
30
  @timeouts = []
31
31
  @block = block
@@ -50,7 +50,7 @@ module Puma
50
50
  @input << client
51
51
  @selector.wakeup
52
52
  true
53
- rescue ClosedQueueError
53
+ rescue ClosedQueueError, IOError # Ignore if selector is already closed
54
54
  false
55
55
  end
56
56
 
@@ -61,12 +61,13 @@ module Puma
61
61
  @selector.wakeup
62
62
  rescue IOError # Ignore if selector is already closed
63
63
  end
64
- @thread.join if @thread
64
+ @thread&.join
65
65
  end
66
66
 
67
67
  private
68
68
 
69
69
  def select_loop
70
+ close_selector = true
70
71
  begin
71
72
  until @input.closed? && @input.empty?
72
73
  # Wakeup any registered object that receives incoming data.
@@ -76,7 +77,7 @@ module Puma
76
77
 
77
78
  # Wakeup all objects that timed out.
78
79
  timed_out = @timeouts.take_while {|t| t.timeout == 0}
79
- timed_out.each(&method(:wakeup!))
80
+ timed_out.each { |c| wakeup! c }
80
81
 
81
82
  unless @input.empty?
82
83
  until @input.empty?
@@ -89,11 +90,19 @@ module Puma
89
90
  rescue StandardError => e
90
91
  STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})"
91
92
  STDERR.puts e.backtrace
92
- retry
93
+
94
+ # NoMethodError may be rarely raised when calling @selector.select, which
95
+ # is odd. Regardless, it may continue for thousands of calls if retried.
96
+ # Also, when it raises, @selector.close also raises an error.
97
+ if NoMethodError === e
98
+ close_selector = false
99
+ else
100
+ retry
101
+ end
93
102
  end
94
103
  # Wakeup all remaining objects on shutdown.
95
104
  @timeouts.each(&@block)
96
- @selector.close
105
+ @selector.close if close_selector
97
106
  end
98
107
 
99
108
  # Start monitoring the object.