puma 6.0.0 → 6.6.0

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +392 -13
  3. data/LICENSE +0 -0
  4. data/README.md +135 -29
  5. data/bin/puma-wild +0 -0
  6. data/docs/architecture.md +0 -0
  7. data/docs/compile_options.md +0 -0
  8. data/docs/deployment.md +0 -0
  9. data/docs/fork_worker.md +11 -1
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/java_options.md +54 -0
  14. data/docs/jungle/README.md +0 -0
  15. data/docs/jungle/rc.d/README.md +0 -0
  16. data/docs/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +12 -0
  18. data/docs/nginx.md +1 -1
  19. data/docs/plugins.md +4 -0
  20. data/docs/rails_dev_mode.md +0 -0
  21. data/docs/restart.md +1 -0
  22. data/docs/signals.md +2 -2
  23. data/docs/stats.md +8 -3
  24. data/docs/systemd.md +13 -7
  25. data/docs/testing_benchmarks_local_files.md +0 -0
  26. data/docs/testing_test_rackup_ci_files.md +0 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  28. data/ext/puma_http11/ext_help.h +0 -0
  29. data/ext/puma_http11/extconf.rb +21 -14
  30. data/ext/puma_http11/http11_parser.c +0 -0
  31. data/ext/puma_http11/http11_parser.h +0 -0
  32. data/ext/puma_http11/http11_parser.java.rl +0 -0
  33. data/ext/puma_http11/http11_parser.rl +0 -0
  34. data/ext/puma_http11/http11_parser_common.rl +0 -0
  35. data/ext/puma_http11/mini_ssl.c +107 -10
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +30 -7
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +0 -0
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +2 -1
  40. data/ext/puma_http11/puma_http11.c +4 -1
  41. data/lib/puma/app/status.rb +1 -1
  42. data/lib/puma/binder.rb +26 -15
  43. data/lib/puma/cli.rb +13 -5
  44. data/lib/puma/client.rb +113 -26
  45. data/lib/puma/cluster/worker.rb +14 -6
  46. data/lib/puma/cluster/worker_handle.rb +4 -5
  47. data/lib/puma/cluster.rb +93 -22
  48. data/lib/puma/commonlogger.rb +21 -14
  49. data/lib/puma/configuration.rb +42 -22
  50. data/lib/puma/const.rb +149 -89
  51. data/lib/puma/control_cli.rb +16 -9
  52. data/lib/puma/detect.rb +5 -4
  53. data/lib/puma/dsl.rb +432 -40
  54. data/lib/puma/error_logger.rb +6 -5
  55. data/lib/puma/events.rb +0 -0
  56. data/lib/puma/io_buffer.rb +10 -0
  57. data/lib/puma/jruby_restart.rb +0 -16
  58. data/lib/puma/json_serialization.rb +0 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +0 -0
  60. data/lib/puma/launcher.rb +29 -29
  61. data/lib/puma/log_writer.rb +23 -13
  62. data/lib/puma/minissl/context_builder.rb +4 -0
  63. data/lib/puma/minissl.rb +23 -0
  64. data/lib/puma/null_io.rb +42 -2
  65. data/lib/puma/plugin/systemd.rb +90 -0
  66. data/lib/puma/plugin/tmp_restart.rb +0 -0
  67. data/lib/puma/plugin.rb +0 -0
  68. data/lib/puma/rack/builder.rb +2 -2
  69. data/lib/puma/rack/urlmap.rb +1 -1
  70. data/lib/puma/rack_default.rb +18 -3
  71. data/lib/puma/reactor.rb +17 -8
  72. data/lib/puma/request.rb +207 -126
  73. data/lib/puma/runner.rb +26 -4
  74. data/lib/puma/sd_notify.rb +146 -0
  75. data/lib/puma/server.rb +121 -49
  76. data/lib/puma/single.rb +3 -1
  77. data/lib/puma/state_file.rb +2 -2
  78. data/lib/puma/thread_pool.rb +56 -9
  79. data/lib/puma/util.rb +1 -1
  80. data/lib/puma.rb +1 -3
  81. data/lib/rack/handler/puma.rb +116 -86
  82. data/tools/Dockerfile +2 -2
  83. data/tools/trickletest.rb +0 -0
  84. metadata +12 -13
  85. data/lib/puma/systemd.rb +0 -47
@@ -15,14 +15,14 @@ module Puma
15
15
 
16
16
  LOG_QUEUE = Queue.new
17
17
 
18
- def initialize(ioerr)
18
+ def initialize(ioerr, env: ENV)
19
19
  @ioerr = ioerr
20
20
 
21
- @debug = ENV.key? 'PUMA_DEBUG'
21
+ @debug = env.key?('PUMA_DEBUG')
22
22
  end
23
23
 
24
- def self.stdio
25
- new $stderr
24
+ def self.stdio(env: ENV)
25
+ new($stderr, env: env)
26
26
  end
27
27
 
28
28
  # Print occurred error details.
@@ -102,7 +102,8 @@ module Puma
102
102
  @ioerr.is_a?(IO) and @ioerr.wait_writable(1)
103
103
  @ioerr.write "#{w_str}\n"
104
104
  @ioerr.flush unless @ioerr.sync
105
- rescue Errno::EPIPE, Errno::EBADF, IOError
105
+ rescue Errno::EPIPE, Errno::EBADF, IOError, Errno::EINVAL
106
+ # 'Invalid argument' (Errno::EINVAL) may be raised by flush
106
107
  end
107
108
  end
108
109
  rescue ThreadError
data/lib/puma/events.rb CHANGED
File without changes
@@ -22,6 +22,16 @@ module Puma
22
22
  read
23
23
  end
24
24
 
25
+ # Read & Reset - returns contents and resets
26
+ # @return [String] StringIO contents
27
+ def read_and_reset
28
+ rewind
29
+ str = read
30
+ truncate 0
31
+ rewind
32
+ str
33
+ end
34
+
25
35
  alias_method :clear, :reset
26
36
 
27
37
  # before Ruby 2.5, `write` would only take one argument
@@ -6,22 +6,6 @@ module Puma
6
6
  module JRubyRestart
7
7
  extend FFI::Library
8
8
  ffi_lib 'c'
9
-
10
- attach_function :execlp, [:string, :varargs], :int
11
9
  attach_function :chdir, [:string], :int
12
- attach_function :fork, [], :int
13
- attach_function :exit, [:int], :void
14
- attach_function :setsid, [], :int
15
-
16
- def self.chdir_exec(dir, argv)
17
- chdir(dir)
18
- cmd = argv.first
19
- argv = ([:string] * argv.size).zip(argv)
20
- argv.flatten!
21
- argv << :string
22
- argv << nil
23
- execlp(cmd, *argv)
24
- raise SystemCallError.new(FFI.errno)
25
- end
26
10
  end
27
11
  end
File without changes
File without changes
data/lib/puma/launcher.rb CHANGED
@@ -46,6 +46,8 @@ module Puma
46
46
  @original_argv = @argv.dup
47
47
  @config = conf
48
48
 
49
+ env = launcher_args.delete(:env) || ENV
50
+
49
51
  @config.options[:log_writer] = @log_writer
50
52
 
51
53
  # Advertise the Configuration
@@ -59,6 +61,13 @@ module Puma
59
61
 
60
62
  @environment = conf.environment
61
63
 
64
+ # Load the systemd integration if we detect systemd's NOTIFY_SOCKET.
65
+ # Skip this on JRuby though, because it is incompatible with the systemd
66
+ # integration due to https://github.com/jruby/jruby/issues/6504
67
+ if ENV["NOTIFY_SOCKET"] && !Puma.jruby? && !ENV["PUMA_SKIP_SYSTEMD"]
68
+ @config.plugins.create('systemd')
69
+ end
70
+
62
71
  if @config.options[:bind_to_activated_sockets]
63
72
  @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
64
73
  @config.options[:binds],
@@ -72,6 +81,8 @@ module Puma
72
81
  @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
73
82
  @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
74
83
 
84
+ @log_writer.custom_logger = options[:custom_logger] if @options[:custom_logger]
85
+
75
86
  generate_restart_data
76
87
 
77
88
  if clustered? && !Puma.forkable?
@@ -96,7 +107,7 @@ module Puma
96
107
 
97
108
  @status = :run
98
109
 
99
- log_config if ENV['PUMA_LOG_CONFIG']
110
+ log_config if env['PUMA_LOG_CONFIG']
100
111
  end
101
112
 
102
113
  attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
@@ -156,6 +167,13 @@ module Puma
156
167
  log "* phased-restart called but not available, restarting normally."
157
168
  return restart
158
169
  end
170
+
171
+ if @options.file_options[:tag].nil?
172
+ dir = File.realdirpath(@restart_dir)
173
+ @options[:tag] = File.basename(dir)
174
+ set_process_title
175
+ end
176
+
159
177
  true
160
178
  end
161
179
 
@@ -180,7 +198,6 @@ module Puma
180
198
 
181
199
  setup_signals
182
200
  set_process_title
183
- integrate_with_systemd
184
201
 
185
202
  # This blocks until the server is stopped
186
203
  @runner.run
@@ -279,7 +296,9 @@ module Puma
279
296
  close_binder_listeners
280
297
 
281
298
  require_relative 'jruby_restart'
282
- JRubyRestart.chdir_exec(@restart_dir, restart_args)
299
+ argv = restart_args
300
+ JRubyRestart.chdir(@restart_dir)
301
+ Kernel.exec(*argv)
283
302
  elsif Puma.windows?
284
303
  close_binder_listeners
285
304
 
@@ -311,27 +330,6 @@ module Puma
311
330
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
312
331
  end
313
332
 
314
- # Puma's systemd integration allows Puma to inform systemd:
315
- # 1. when it has successfully started
316
- # 2. when it is starting shutdown
317
- # 3. periodically for a liveness check with a watchdog thread
318
- def integrate_with_systemd
319
- return unless ENV["NOTIFY_SOCKET"]
320
-
321
- begin
322
- require_relative 'systemd'
323
- rescue LoadError
324
- log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
325
- return
326
- end
327
-
328
- log "* Enabling systemd notification integration"
329
-
330
- systemd = Systemd.new(@log_writer, @events)
331
- systemd.hook_events
332
- systemd.start_watchdog
333
- end
334
-
335
333
  def log(str)
336
334
  @log_writer.log(str)
337
335
  end
@@ -421,12 +419,14 @@ module Puma
421
419
  end
422
420
 
423
421
  def setup_signals
424
- begin
425
- Signal.trap "SIGUSR2" do
426
- restart
422
+ unless ENV["PUMA_SKIP_SIGUSR2"]
423
+ begin
424
+ Signal.trap "SIGUSR2" do
425
+ restart
426
+ end
427
+ rescue Exception
428
+ log "*** SIGUSR2 not implemented, signal based restart unavailable!"
427
429
  end
428
- rescue Exception
429
- log "*** SIGUSR2 not implemented, signal based restart unavailable!"
430
430
  end
431
431
 
432
432
  unless Puma.jruby?
@@ -28,38 +28,43 @@ module Puma
28
28
  attr_reader :stdout,
29
29
  :stderr
30
30
 
31
- attr_accessor :formatter
31
+ attr_accessor :formatter, :custom_logger
32
32
 
33
33
  # Create a LogWriter that prints to +stdout+ and +stderr+.
34
- def initialize(stdout, stderr)
34
+ def initialize(stdout, stderr, env: ENV)
35
35
  @formatter = DefaultFormatter.new
36
+ @custom_logger = nil
36
37
  @stdout = stdout
37
38
  @stderr = stderr
38
39
 
39
- @debug = ENV.key?('PUMA_DEBUG')
40
- @error_logger = ErrorLogger.new(@stderr)
40
+ @debug = env.key?('PUMA_DEBUG')
41
+ @error_logger = ErrorLogger.new(@stderr, env: env)
41
42
  end
42
43
 
43
44
  DEFAULT = new(STDOUT, STDERR)
44
45
 
45
46
  # Returns an LogWriter object which writes its status to
46
47
  # two StringIO objects.
47
- def self.strings
48
- LogWriter.new(StringIO.new, StringIO.new)
48
+ def self.strings(env: ENV)
49
+ LogWriter.new(StringIO.new, StringIO.new, env: env)
49
50
  end
50
51
 
51
- def self.stdio
52
- LogWriter.new($stdout, $stderr)
52
+ def self.stdio(env: ENV)
53
+ LogWriter.new($stdout, $stderr, env: env)
53
54
  end
54
55
 
55
- def self.null
56
+ def self.null(env: ENV)
56
57
  n = NullIO.new
57
- LogWriter.new(n, n)
58
+ LogWriter.new(n, n, env: env)
58
59
  end
59
60
 
60
61
  # Write +str+ to +@stdout+
61
62
  def log(str)
62
- internal_write "#{@formatter.call str}\n"
63
+ if @custom_logger&.respond_to?(:write)
64
+ @custom_logger.write(format(str))
65
+ else
66
+ internal_write "#{@formatter.call str}\n"
67
+ end
63
68
  end
64
69
 
65
70
  def write(str)
@@ -73,13 +78,18 @@ module Puma
73
78
  @stdout.is_a?(IO) and @stdout.wait_writable(1)
74
79
  @stdout.write w_str
75
80
  @stdout.flush unless @stdout.sync
76
- rescue Errno::EPIPE, Errno::EBADF, IOError
81
+ rescue Errno::EPIPE, Errno::EBADF, IOError, Errno::EINVAL
82
+ # 'Invalid argument' (Errno::EINVAL) may be raised by flush
77
83
  end
78
84
  end
79
85
  rescue ThreadError
80
86
  end
81
87
  private :internal_write
82
88
 
89
+ def debug?
90
+ @debug
91
+ end
92
+
83
93
  def debug(str)
84
94
  log("% #{str}") if @debug
85
95
  end
@@ -115,7 +125,7 @@ module Puma
115
125
  def ssl_error(error, ssl_socket)
116
126
  peeraddr = ssl_socket.peeraddr.last rescue "<unknown>"
117
127
  peercert = ssl_socket.peercert
118
- subject = peercert ? peercert.subject : nil
128
+ subject = peercert&.subject
119
129
  @error_logger.info(error: error, text: "SSL error, peer: #{peeraddr}, peer cert: #{subject}")
120
130
  end
121
131
 
@@ -38,6 +38,7 @@ module Puma
38
38
 
39
39
  ctx.key = params['key'] if params['key']
40
40
  ctx.key_pem = params['key_pem'] if params['key_pem']
41
+ ctx.key_password_command = params['key_password_command'] if params['key_password_command']
41
42
 
42
43
  if params['cert'].nil? && params['cert_pem'].nil?
43
44
  log_writer.error "Please specify the SSL cert via 'cert=' or 'cert_pem='"
@@ -50,10 +51,13 @@ module Puma
50
51
  unless params['ca']
51
52
  log_writer.error "Please specify the SSL ca via 'ca='"
52
53
  end
54
+ # needed for Puma::MiniSSL::Socket#peercert, env['puma.peercert']
55
+ require 'openssl'
53
56
  end
54
57
 
55
58
  ctx.ca = params['ca'] if params['ca']
56
59
  ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
60
+ ctx.ssl_ciphersuites = params['ssl_ciphersuites'] if params['ssl_ciphersuites'] && HAS_TLS1_3
57
61
 
58
62
  ctx.reuse = params['reuse'] if params['reuse']
59
63
  end
data/lib/puma/minissl.rb CHANGED
@@ -5,6 +5,7 @@ begin
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`
9
10
  # use require, see https://github.com/puma/puma/pull/2381
10
11
  require 'puma/puma_http11'
@@ -183,6 +184,11 @@ module Puma
183
184
  @socket.peeraddr
184
185
  end
185
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]
186
192
  # @!attribute [r] peercert
187
193
  def peercert
188
194
  return @peercert if @peercert
@@ -277,11 +283,13 @@ module Puma
277
283
  else
278
284
  # non-jruby Context properties
279
285
  attr_reader :key
286
+ attr_reader :key_password_command
280
287
  attr_reader :cert
281
288
  attr_reader :ca
282
289
  attr_reader :cert_pem
283
290
  attr_reader :key_pem
284
291
  attr_accessor :ssl_cipher_filter
292
+ attr_accessor :ssl_ciphersuites
285
293
  attr_accessor :verification_flags
286
294
 
287
295
  attr_reader :reuse, :reuse_cache_size, :reuse_timeout
@@ -291,6 +299,10 @@ module Puma
291
299
  @key = key
292
300
  end
293
301
 
302
+ def key_password_command=(key_password_command)
303
+ @key_password_command = key_password_command
304
+ end
305
+
294
306
  def cert=(cert)
295
307
  check_file cert, 'Cert'
296
308
  @cert = cert
@@ -316,6 +328,17 @@ module Puma
316
328
  raise "Cert not configured" if @cert.nil? && @cert_pem.nil?
317
329
  end
318
330
 
331
+ # Executes the command to return the password needed to decrypt the key.
332
+ def key_password
333
+ raise "Key password command not configured" if @key_password_command.nil?
334
+
335
+ stdout_str, stderr_str, status = Open3.capture3(@key_password_command)
336
+
337
+ return stdout_str.chomp if status.success?
338
+
339
+ raise "Key password failed with code #{status.exitstatus}: #{stderr_str}"
340
+ end
341
+
319
342
  # Controls session reuse. Allowed values are as follows:
320
343
  # * 'off' - matches the behavior of Puma 5.6 and earlier. This is included
321
344
  # in case reuse 'on' is made the default in future Puma versions.
data/lib/puma/null_io.rb CHANGED
@@ -16,15 +16,38 @@ module Puma
16
16
  def each
17
17
  end
18
18
 
19
+ def pos
20
+ 0
21
+ end
22
+
19
23
  # Mimics IO#read with no data.
20
24
  #
21
- def read(count = nil, _buffer = nil)
22
- count && count > 0 ? nil : ""
25
+ def read(length = nil, buffer = nil)
26
+ if length.to_i < 0
27
+ raise ArgumentError, "(negative length #{length} given)"
28
+ end
29
+
30
+ buffer = if buffer.nil?
31
+ "".b
32
+ else
33
+ String.try_convert(buffer) or raise TypeError, "no implicit conversion of #{buffer.class} into String"
34
+ end
35
+ buffer.clear
36
+ if length.to_i > 0
37
+ nil
38
+ else
39
+ buffer
40
+ end
23
41
  end
24
42
 
25
43
  def rewind
26
44
  end
27
45
 
46
+ def seek(pos, whence = 0)
47
+ raise ArgumentError, "negative length #{pos} given" if pos.negative?
48
+ 0
49
+ end
50
+
28
51
  def close
29
52
  end
30
53
 
@@ -57,5 +80,22 @@ module Puma
57
80
  def closed?
58
81
  false
59
82
  end
83
+
84
+ def set_encoding(enc)
85
+ self
86
+ end
87
+
88
+ # per rack spec
89
+ def external_encoding
90
+ Encoding::ASCII_8BIT
91
+ end
92
+
93
+ def binmode
94
+ self
95
+ end
96
+
97
+ def binmode?
98
+ true
99
+ end
60
100
  end
61
101
  end
@@ -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
File without changes
data/lib/puma/plugin.rb CHANGED
File without changes
@@ -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)
@@ -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, _, _)|
@@ -2,8 +2,23 @@
2
2
 
3
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_relative '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
 
@@ -67,6 +67,7 @@ module Puma
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.
@@ -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.