puma 5.2.2 → 6.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +483 -4
  3. data/README.md +101 -20
  4. data/bin/puma-wild +1 -1
  5. data/docs/architecture.md +50 -16
  6. data/docs/compile_options.md +38 -2
  7. data/docs/deployment.md +53 -67
  8. data/docs/fork_worker.md +1 -3
  9. data/docs/jungle/rc.d/README.md +1 -1
  10. data/docs/kubernetes.md +1 -1
  11. data/docs/nginx.md +1 -1
  12. data/docs/plugins.md +15 -15
  13. data/docs/rails_dev_mode.md +2 -3
  14. data/docs/restart.md +7 -7
  15. data/docs/signals.md +11 -10
  16. data/docs/stats.md +8 -8
  17. data/docs/systemd.md +65 -69
  18. data/docs/testing_benchmarks_local_files.md +150 -0
  19. data/docs/testing_test_rackup_ci_files.md +36 -0
  20. data/ext/puma_http11/extconf.rb +44 -13
  21. data/ext/puma_http11/http11_parser.c +24 -11
  22. data/ext/puma_http11/http11_parser.h +2 -2
  23. data/ext/puma_http11/http11_parser.java.rl +2 -2
  24. data/ext/puma_http11/http11_parser.rl +2 -2
  25. data/ext/puma_http11/http11_parser_common.rl +3 -3
  26. data/ext/puma_http11/mini_ssl.c +150 -23
  27. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  28. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +50 -48
  29. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +188 -102
  30. data/ext/puma_http11/puma_http11.c +18 -10
  31. data/lib/puma/app/status.rb +10 -7
  32. data/lib/puma/binder.rb +112 -62
  33. data/lib/puma/cli.rb +24 -20
  34. data/lib/puma/client.rb +162 -36
  35. data/lib/puma/cluster/worker.rb +31 -27
  36. data/lib/puma/cluster/worker_handle.rb +12 -1
  37. data/lib/puma/cluster.rb +102 -61
  38. data/lib/puma/commonlogger.rb +21 -14
  39. data/lib/puma/configuration.rb +78 -54
  40. data/lib/puma/const.rb +135 -97
  41. data/lib/puma/control_cli.rb +25 -20
  42. data/lib/puma/detect.rb +12 -2
  43. data/lib/puma/dsl.rb +308 -58
  44. data/lib/puma/error_logger.rb +20 -11
  45. data/lib/puma/events.rb +6 -126
  46. data/lib/puma/io_buffer.rb +39 -4
  47. data/lib/puma/jruby_restart.rb +2 -1
  48. data/lib/puma/{json.rb → json_serialization.rb} +1 -1
  49. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  50. data/lib/puma/launcher.rb +114 -173
  51. data/lib/puma/log_writer.rb +147 -0
  52. data/lib/puma/minissl/context_builder.rb +30 -16
  53. data/lib/puma/minissl.rb +132 -38
  54. data/lib/puma/null_io.rb +5 -0
  55. data/lib/puma/plugin/systemd.rb +90 -0
  56. data/lib/puma/plugin/tmp_restart.rb +1 -1
  57. data/lib/puma/plugin.rb +2 -2
  58. data/lib/puma/rack/builder.rb +7 -7
  59. data/lib/puma/rack_default.rb +19 -4
  60. data/lib/puma/reactor.rb +19 -10
  61. data/lib/puma/request.rb +373 -153
  62. data/lib/puma/runner.rb +74 -28
  63. data/lib/puma/sd_notify.rb +149 -0
  64. data/lib/puma/server.rb +127 -136
  65. data/lib/puma/single.rb +13 -11
  66. data/lib/puma/state_file.rb +39 -7
  67. data/lib/puma/thread_pool.rb +33 -26
  68. data/lib/puma/util.rb +20 -15
  69. data/lib/puma.rb +28 -11
  70. data/lib/rack/handler/puma.rb +113 -86
  71. data/tools/Dockerfile +1 -1
  72. metadata +15 -10
  73. data/lib/puma/queue_close.rb +0 -26
  74. data/lib/puma/systemd.rb +0 -46
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
@@ -161,30 +164,15 @@ module Puma
161
164
  @socket.flush
162
165
  end
163
166
 
164
- def read_and_drop(timeout = 1)
165
- return :timeout unless IO.select([@socket], nil, nil, timeout)
166
- case @socket.read_nonblock(1024, exception: false)
167
- when nil
168
- :eof
169
- when :wait_readable
170
- :eagain
171
- else
172
- :drop
173
- end
174
- end
175
-
176
- def should_drop_bytes?
177
- @engine.init? || !@engine.shutdown
178
- end
179
-
180
167
  def close
181
168
  begin
182
- # Read any drop any partially initialized sockets and any received bytes during shutdown.
183
- # Don't let this socket hold this loop forever.
184
- # If it can't send more packets within 1s, then give up.
185
- return if [:timeout, :eof].include?(read_and_drop(1)) while should_drop_bytes?
169
+ unless @engine.shutdown
170
+ while alert_data = @engine.extract
171
+ @socket.write alert_data
172
+ end
173
+ end
186
174
  rescue IOError, SystemCallError
187
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
175
+ Puma::Util.purge_interrupt_queue
188
176
  # nothing
189
177
  ensure
190
178
  @socket.close
@@ -210,10 +198,6 @@ module Puma
210
198
  if IS_JRUBY
211
199
  OPENSSL_NO_SSL3 = false
212
200
  OPENSSL_NO_TLS1 = false
213
-
214
- class SSLError < StandardError
215
- # Define this for jruby even though it isn't used.
216
- end
217
201
  end
218
202
 
219
203
  class Context
@@ -223,49 +207,159 @@ module Puma
223
207
  def initialize
224
208
  @no_tlsv1 = false
225
209
  @no_tlsv1_1 = false
210
+ @key = nil
211
+ @cert = nil
212
+ @key_pem = nil
213
+ @cert_pem = nil
214
+ @reuse = nil
215
+ @reuse_cache_size = nil
216
+ @reuse_timeout = nil
217
+ end
218
+
219
+ def check_file(file, desc)
220
+ raise ArgumentError, "#{desc} file '#{file}' does not exist" unless File.exist? file
221
+ raise ArgumentError, "#{desc} file '#{file}' is not readable" unless File.readable? file
226
222
  end
227
223
 
228
224
  if IS_JRUBY
229
225
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
230
226
  attr_reader :keystore
227
+ attr_reader :keystore_type
231
228
  attr_accessor :keystore_pass
232
- attr_accessor :ssl_cipher_list
229
+ attr_reader :truststore
230
+ attr_reader :truststore_type
231
+ attr_accessor :truststore_pass
232
+ attr_reader :cipher_suites
233
+ attr_reader :protocols
233
234
 
234
235
  def keystore=(keystore)
235
- raise ArgumentError, "No such keystore file '#{keystore}'" unless File.exist? keystore
236
+ check_file keystore, 'Keystore'
236
237
  @keystore = keystore
237
238
  end
238
239
 
240
+ def truststore=(truststore)
241
+ # NOTE: historically truststore was assumed the same as keystore, this is kept for backwards
242
+ # compatibility, to rely on JVM's trust defaults we allow setting `truststore = :default`
243
+ unless truststore.eql?(:default)
244
+ raise ArgumentError, "No such truststore file '#{truststore}'" unless File.exist?(truststore)
245
+ end
246
+ @truststore = truststore
247
+ end
248
+
249
+ def keystore_type=(type)
250
+ raise ArgumentError, "Invalid keystore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
251
+ @keystore_type = type
252
+ end
253
+
254
+ def truststore_type=(type)
255
+ raise ArgumentError, "Invalid truststore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
256
+ @truststore_type = type
257
+ end
258
+
259
+ def cipher_suites=(list)
260
+ list = list.split(',').map(&:strip) if list.is_a?(String)
261
+ @cipher_suites = list
262
+ end
263
+
264
+ # aliases for backwards compatibility
265
+ alias_method :ssl_cipher_list, :cipher_suites
266
+ alias_method :ssl_cipher_list=, :cipher_suites=
267
+
268
+ def protocols=(list)
269
+ list = list.split(',').map(&:strip) if list.is_a?(String)
270
+ @protocols = list
271
+ end
272
+
239
273
  def check
240
274
  raise "Keystore not configured" unless @keystore
275
+ # @truststore defaults to @keystore due backwards compatibility
241
276
  end
242
277
 
243
278
  else
244
279
  # non-jruby Context properties
245
280
  attr_reader :key
281
+ attr_reader :key_password_command
246
282
  attr_reader :cert
247
283
  attr_reader :ca
284
+ attr_reader :cert_pem
285
+ attr_reader :key_pem
248
286
  attr_accessor :ssl_cipher_filter
249
287
  attr_accessor :verification_flags
250
288
 
289
+ attr_reader :reuse, :reuse_cache_size, :reuse_timeout
290
+
251
291
  def key=(key)
252
- raise ArgumentError, "No such key file '#{key}'" unless File.exist? key
292
+ check_file key, 'Key'
253
293
  @key = key
254
294
  end
255
295
 
296
+ def key_password_command=(key_password_command)
297
+ @key_password_command = key_password_command
298
+ end
299
+
256
300
  def cert=(cert)
257
- raise ArgumentError, "No such cert file '#{cert}'" unless File.exist? cert
301
+ check_file cert, 'Cert'
258
302
  @cert = cert
259
303
  end
260
304
 
261
305
  def ca=(ca)
262
- raise ArgumentError, "No such ca file '#{ca}'" unless File.exist? ca
306
+ check_file ca, 'ca'
263
307
  @ca = ca
264
308
  end
265
309
 
310
+ def cert_pem=(cert_pem)
311
+ raise ArgumentError, "'cert_pem' is not a String" unless cert_pem.is_a? String
312
+ @cert_pem = cert_pem
313
+ end
314
+
315
+ def key_pem=(key_pem)
316
+ raise ArgumentError, "'key_pem' is not a String" unless key_pem.is_a? String
317
+ @key_pem = key_pem
318
+ end
319
+
266
320
  def check
267
- raise "Key not configured" unless @key
268
- raise "Cert not configured" unless @cert
321
+ raise "Key not configured" if @key.nil? && @key_pem.nil?
322
+ raise "Cert not configured" if @cert.nil? && @cert_pem.nil?
323
+ end
324
+
325
+ # Executes the command to return the password needed to decrypt the key.
326
+ def key_password
327
+ raise "Key password command not configured" if @key_password_command.nil?
328
+
329
+ stdout_str, stderr_str, status = Open3.capture3(@key_password_command)
330
+
331
+ return stdout_str.chomp if status.success?
332
+
333
+ raise "Key password failed with code #{status.exitstatus}: #{stderr_str}"
334
+ end
335
+
336
+ # Controls session reuse. Allowed values are as follows:
337
+ # * 'off' - matches the behavior of Puma 5.6 and earlier. This is included
338
+ # in case reuse 'on' is made the default in future Puma versions.
339
+ # * 'dflt' - sets session reuse on, with OpenSSL default cache size of
340
+ # 20k and default timeout of 300 seconds.
341
+ # * 's,t' - where s and t are integer strings, for size and timeout.
342
+ # * 's' - where s is an integer strings for size.
343
+ # * ',t' - where t is an integer strings for timeout.
344
+ #
345
+ def reuse=(reuse_str)
346
+ case reuse_str
347
+ when 'off'
348
+ @reuse = nil
349
+ when 'dflt'
350
+ @reuse = true
351
+ when /\A\d+\z/
352
+ @reuse = true
353
+ @reuse_cache_size = reuse_str.to_i
354
+ when /\A\d+,\d+\z/
355
+ @reuse = true
356
+ size, time = reuse_str.split ','
357
+ @reuse_cache_size = size.to_i
358
+ @reuse_timeout = time.to_i
359
+ when /\A,\d+\z/
360
+ @reuse = true
361
+ @reuse_timeout = reuse_str.delete(',').to_i
362
+ end
269
363
  end
270
364
  end
271
365
 
data/lib/puma/null_io.rb CHANGED
@@ -52,5 +52,10 @@ module Puma
52
52
  def flush
53
53
  self
54
54
  end
55
+
56
+ # This is used as singleton class, so can't have state.
57
+ def closed?
58
+ false
59
+ end
55
60
  end
56
61
  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
@@ -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)
data/lib/puma/plugin.rb CHANGED
@@ -64,7 +64,7 @@ module Puma
64
64
  def fire_background
65
65
  @background.each_with_index do |b, i|
66
66
  Thread.new do
67
- Puma.set_thread_name "plugin background #{i}"
67
+ Puma.set_thread_name "plgn bg #{i}"
68
68
  b.call
69
69
  end
70
70
  end
@@ -91,7 +91,7 @@ module Puma
91
91
  path = ary.first[CALLER_FILE]
92
92
 
93
93
  m = %r!puma/plugin/([^/]*)\.rb$!.match(path)
94
- return m[1]
94
+ m[1]
95
95
  end
96
96
 
97
97
  def self.create(&blk)
@@ -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
@@ -165,7 +165,7 @@ module Puma::Rack
165
165
  require config
166
166
  app = Object.const_get(::File.basename(config, '.rb').capitalize)
167
167
  end
168
- return app, options
168
+ [app, options]
169
169
  end
170
170
 
171
171
  def self.new_from_string(builder_script, file="(rackup)")
@@ -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 }
@@ -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.