puma 5.6.4-java → 6.0.0-java

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +136 -3
  3. data/README.md +21 -17
  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/testing_benchmarks_local_files.md +150 -0
  8. data/docs/testing_test_rackup_ci_files.md +36 -0
  9. data/ext/puma_http11/extconf.rb +18 -10
  10. data/ext/puma_http11/http11_parser.c +1 -1
  11. data/ext/puma_http11/http11_parser.h +1 -1
  12. data/ext/puma_http11/http11_parser.java.rl +2 -2
  13. data/ext/puma_http11/http11_parser.rl +2 -2
  14. data/ext/puma_http11/http11_parser_common.rl +2 -2
  15. data/ext/puma_http11/mini_ssl.c +63 -24
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  17. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  18. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +166 -65
  19. data/ext/puma_http11/puma_http11.c +17 -9
  20. data/lib/puma/app/status.rb +6 -3
  21. data/lib/puma/binder.rb +37 -43
  22. data/lib/puma/cli.rb +11 -17
  23. data/lib/puma/client.rb +22 -12
  24. data/lib/puma/cluster/worker.rb +13 -11
  25. data/lib/puma/cluster/worker_handle.rb +4 -1
  26. data/lib/puma/cluster.rb +28 -25
  27. data/lib/puma/configuration.rb +74 -58
  28. data/lib/puma/const.rb +14 -18
  29. data/lib/puma/control_cli.rb +21 -18
  30. data/lib/puma/detect.rb +2 -0
  31. data/lib/puma/dsl.rb +94 -49
  32. data/lib/puma/error_logger.rb +17 -9
  33. data/lib/puma/events.rb +6 -126
  34. data/lib/puma/io_buffer.rb +29 -4
  35. data/lib/puma/jruby_restart.rb +2 -1
  36. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  37. data/lib/puma/launcher.rb +107 -156
  38. data/lib/puma/log_writer.rb +137 -0
  39. data/lib/puma/minissl/context_builder.rb +23 -12
  40. data/lib/puma/minissl.rb +91 -15
  41. data/lib/puma/null_io.rb +5 -0
  42. data/lib/puma/plugin/tmp_restart.rb +1 -1
  43. data/lib/puma/puma_http11.jar +0 -0
  44. data/lib/puma/rack/builder.rb +4 -4
  45. data/lib/puma/rack_default.rb +1 -1
  46. data/lib/puma/reactor.rb +3 -3
  47. data/lib/puma/request.rb +291 -156
  48. data/lib/puma/runner.rb +41 -20
  49. data/lib/puma/server.rb +53 -64
  50. data/lib/puma/single.rb +10 -10
  51. data/lib/puma/state_file.rb +2 -4
  52. data/lib/puma/systemd.rb +3 -2
  53. data/lib/puma/thread_pool.rb +16 -13
  54. data/lib/puma/util.rb +12 -14
  55. data/lib/puma.rb +11 -8
  56. data/lib/rack/handler/puma.rb +9 -9
  57. metadata +7 -3
  58. data/lib/puma/queue_close.rb +0 -26
data/lib/puma/cluster.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/runner'
4
- require 'puma/util'
5
- require 'puma/plugin'
6
- require 'puma/cluster/worker_handle'
7
- require 'puma/cluster/worker'
3
+ require_relative 'runner'
4
+ require_relative 'util'
5
+ require_relative 'plugin'
6
+ require_relative 'cluster/worker_handle'
7
+ require_relative 'cluster/worker'
8
8
 
9
9
  require 'time'
10
10
 
@@ -17,8 +17,8 @@ module Puma
17
17
  # via the `spawn_workers` method call. Each worker will have it's own
18
18
  # instance of a `Puma::Server`.
19
19
  class Cluster < Runner
20
- def initialize(cli, events)
21
- super cli, events
20
+ def initialize(launcher)
21
+ super(launcher)
22
22
 
23
23
  @phase = 0
24
24
  @workers = []
@@ -27,6 +27,10 @@ module Puma
27
27
  @phased_restart = false
28
28
  end
29
29
 
30
+ # Returns the list of cluster worker handles.
31
+ # @return [Array<Puma::Cluster::WorkerHandle>]
32
+ attr_reader :workers
33
+
30
34
  def stop_workers
31
35
  log "- Gracefully shutting down workers..."
32
36
  @workers.each { |x| x.term }
@@ -92,7 +96,7 @@ module Puma
92
96
 
93
97
  # @version 5.0.0
94
98
  def spawn_worker(idx, master)
95
- @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
99
+ @config.run_hooks(:before_worker_fork, idx, @log_writer)
96
100
 
97
101
  pid = fork { worker(idx, master) }
98
102
  if !pid
@@ -101,7 +105,7 @@ module Puma
101
105
  exit! 1
102
106
  end
103
107
 
104
- @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
108
+ @config.run_hooks(:after_worker_fork, idx, @log_writer)
105
109
  pid
106
110
  end
107
111
 
@@ -176,10 +180,10 @@ module Puma
176
180
  end
177
181
  end
178
182
 
179
- @next_check = [
180
- @workers.reject(&:term?).map(&:ping_timeout).min,
181
- @next_check
182
- ].compact.min
183
+ t = @workers.reject(&:term?)
184
+ t.map!(&:ping_timeout)
185
+
186
+ @next_check = [t.min, @next_check].compact.min
183
187
  end
184
188
 
185
189
  def worker(index, master)
@@ -209,8 +213,8 @@ module Puma
209
213
  stop
210
214
  end
211
215
 
212
- def phased_restart
213
- return false if @options[:preload_app]
216
+ def phased_restart(refork = false)
217
+ return false if @options[:preload_app] && !refork
214
218
 
215
219
  @phased_restart = true
216
220
  wakeup!
@@ -226,7 +230,7 @@ module Puma
226
230
  def stop_blocked
227
231
  @status = :stop if @status == :run
228
232
  wakeup!
229
- @control.stop(true) if @control
233
+ @control&.stop true
230
234
  Process.waitall
231
235
  end
232
236
 
@@ -265,7 +269,7 @@ module Puma
265
269
  booted_workers: worker_status.count { |w| w[:booted] },
266
270
  old_workers: old_worker_count,
267
271
  worker_status: worker_status,
268
- }
272
+ }.merge(super)
269
273
  end
270
274
 
271
275
  def preload?
@@ -277,7 +281,7 @@ module Puma
277
281
  if (worker = @workers.find { |w| w.index == 0 })
278
282
  worker.phase += 1
279
283
  end
280
- phased_restart
284
+ phased_restart(true)
281
285
  end
282
286
 
283
287
  # We do this in a separate method to keep the lambda scope
@@ -290,7 +294,7 @@ module Puma
290
294
 
291
295
  # Auto-fork after the specified number of requests.
292
296
  if (fork_requests = @options[:fork_worker].to_i) > 0
293
- @launcher.events.register(:ping!) do |w|
297
+ @events.register(:ping!) do |w|
294
298
  fork_worker! if w.index == 0 &&
295
299
  w.phase == 0 &&
296
300
  w.last_status[:requests_count] >= fork_requests
@@ -372,12 +376,12 @@ module Puma
372
376
  else
373
377
  log "* Restarts: (\u2714) hot (\u2714) phased"
374
378
 
375
- unless @launcher.config.app_configured?
379
+ unless @config.app_configured?
376
380
  error "No application configured, nothing to run"
377
381
  exit 1
378
382
  end
379
383
 
380
- @launcher.binder.parse @options[:binds], self
384
+ @launcher.binder.parse @options[:binds]
381
385
  end
382
386
 
383
387
  read, @wakeup = Puma::Util.pipe
@@ -409,8 +413,7 @@ module Puma
409
413
 
410
414
  @master_read, @worker_write = read, @wakeup
411
415
 
412
- @launcher.config.run_hooks :before_fork, nil, @launcher.events
413
- Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
416
+ @config.run_hooks(:before_fork, nil, @log_writer)
414
417
 
415
418
  spawn_workers
416
419
 
@@ -463,9 +466,9 @@ module Puma
463
466
  w.term unless w.term?
464
467
  when "p"
465
468
  w.ping!(result.sub(/^\d+/,'').chomp)
466
- @launcher.events.fire(:ping!, w)
469
+ @events.fire(:ping!, w)
467
470
  if !booted && @workers.none? {|worker| worker.last_status.empty?}
468
- @launcher.events.fire_on_booted!
471
+ @events.fire_on_booted!
469
472
  booted = true
470
473
  end
471
474
  end
@@ -1,21 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/rack/builder'
4
- require 'puma/plugin'
5
- require 'puma/const'
3
+ require_relative 'rack/builder'
4
+ require_relative 'plugin'
5
+ require_relative 'const'
6
+ # note that dsl is loaded at end of file, requires ConfigDefault constants
6
7
 
7
8
  module Puma
8
-
9
- module ConfigDefault
10
- DefaultRackup = "config.ru"
11
-
12
- DefaultTCPHost = "0.0.0.0"
13
- DefaultTCPPort = 9292
14
- DefaultWorkerCheckInterval = 5
15
- DefaultWorkerTimeout = 60
16
- DefaultWorkerShutdownTimeout = 30
17
- end
18
-
19
9
  # A class used for storing "leveled" configuration options.
20
10
  #
21
11
  # In this class any "user" specified options take precedence over any
@@ -136,7 +126,48 @@ module Puma
136
126
  # is done because an environment variable may have been modified while loading
137
127
  # configuration files.
138
128
  class Configuration
139
- include ConfigDefault
129
+ DEFAULTS = {
130
+ auto_trim_time: 30,
131
+ binds: ['tcp://0.0.0.0:9292'.freeze],
132
+ clean_thread_locals: false,
133
+ debug: false,
134
+ early_hints: nil,
135
+ environment: 'development'.freeze,
136
+ # Number of seconds to wait until we get the first data for the request
137
+ first_data_timeout: 30,
138
+ io_selector_backend: :auto,
139
+ log_requests: false,
140
+ logger: STDOUT,
141
+ # How many requests to attempt inline before sending a client back to
142
+ # the reactor to be subject to normal ordering. The idea here is that
143
+ # we amortize the cost of going back to the reactor for a well behaved
144
+ # but very "greedy" client across 10 requests. This prevents a not
145
+ # well behaved client from monopolizing the thread forever.
146
+ max_fast_inline: 10,
147
+ max_threads: Puma.mri? ? 5 : 16,
148
+ min_threads: 0,
149
+ mode: :http,
150
+ mutate_stdout_and_stderr_to_sync_on_write: true,
151
+ out_of_band: [],
152
+ # Number of seconds for another request within a persistent session.
153
+ persistent_timeout: 20,
154
+ queue_requests: true,
155
+ rackup: 'config.ru'.freeze,
156
+ raise_exception_on_sigterm: true,
157
+ reaping_time: 1,
158
+ remote_address: :socket,
159
+ silence_single_worker_warning: false,
160
+ tag: File.basename(Dir.getwd),
161
+ tcp_host: '0.0.0.0'.freeze,
162
+ tcp_port: 9292,
163
+ wait_for_less_busy_worker: 0.005,
164
+ worker_boot_timeout: 60,
165
+ worker_check_interval: 5,
166
+ worker_culling_strategy: :youngest,
167
+ worker_shutdown_timeout: 30,
168
+ worker_timeout: 60,
169
+ workers: 0,
170
+ }
140
171
 
141
172
  def initialize(user_options={}, default_options = {}, &block)
142
173
  default_options = self.puma_default_options.merge(default_options)
@@ -181,37 +212,22 @@ module Puma
181
212
  self
182
213
  end
183
214
 
184
- # @version 5.0.0
185
- def default_max_threads
186
- Puma.mri? ? 5 : 16
215
+ def puma_default_options
216
+ defaults = DEFAULTS.dup
217
+ puma_options_from_env.each { |k,v| defaults[k] = v if v }
218
+ defaults
187
219
  end
188
220
 
189
- def puma_default_options
221
+ def puma_options_from_env
222
+ min = ENV['PUMA_MIN_THREADS'] || ENV['MIN_THREADS']
223
+ max = ENV['PUMA_MAX_THREADS'] || ENV['MAX_THREADS']
224
+ workers = ENV['WEB_CONCURRENCY']
225
+
190
226
  {
191
- :min_threads => Integer(ENV['PUMA_MIN_THREADS'] || ENV['MIN_THREADS'] || 0),
192
- :max_threads => Integer(ENV['PUMA_MAX_THREADS'] || ENV['MAX_THREADS'] || default_max_threads),
193
- :log_requests => false,
194
- :debug => false,
195
- :binds => ["tcp://#{DefaultTCPHost}:#{DefaultTCPPort}"],
196
- :workers => Integer(ENV['WEB_CONCURRENCY'] || 0),
197
- :silence_single_worker_warning => false,
198
- :mode => :http,
199
- :worker_check_interval => DefaultWorkerCheckInterval,
200
- :worker_timeout => DefaultWorkerTimeout,
201
- :worker_boot_timeout => DefaultWorkerTimeout,
202
- :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
203
- :worker_culling_strategy => :youngest,
204
- :remote_address => :socket,
205
- :tag => method(:infer_tag),
206
- :environment => -> { ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development' },
207
- :rackup => DefaultRackup,
208
- :logger => STDOUT,
209
- :persistent_timeout => Const::PERSISTENT_TIMEOUT,
210
- :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
211
- :raise_exception_on_sigterm => true,
212
- :max_fast_inline => Const::MAX_FAST_INLINE,
213
- :io_selector_backend => :auto,
214
- :mutate_stdout_and_stderr_to_sync_on_write => true,
227
+ min_threads: min && Integer(min),
228
+ max_threads: max && Integer(max),
229
+ workers: workers && Integer(workers),
230
+ environment: ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV'],
215
231
  }
216
232
  end
217
233
 
@@ -227,7 +243,7 @@ module Puma
227
243
  return [] if files == ['-']
228
244
  return files if files.any?
229
245
 
230
- first_default_file = %W(config/puma/#{environment_str}.rb config/puma.rb).find do |f|
246
+ first_default_file = %W(config/puma/#{@options[:environment]}.rb config/puma.rb).find do |f|
231
247
  File.exist?(f)
232
248
  end
233
249
 
@@ -270,7 +286,7 @@ module Puma
270
286
  found = options[:app] || load_rackup
271
287
 
272
288
  if @options[:log_requests]
273
- require 'puma/commonlogger'
289
+ require_relative 'commonlogger'
274
290
  logger = @options[:logger]
275
291
  found = CommonLogger.new(found, logger)
276
292
  end
@@ -283,21 +299,25 @@ module Puma
283
299
  @options[:environment]
284
300
  end
285
301
 
286
- def environment_str
287
- environment.respond_to?(:call) ? environment.call : environment
288
- end
289
-
290
302
  def load_plugin(name)
291
303
  @plugins.create name
292
304
  end
293
305
 
294
- def run_hooks(key, arg, events)
306
+ # @param key [:Symbol] hook to run
307
+ # @param arg [Launcher, Int] `:on_restart` passes Launcher
308
+ #
309
+ def run_hooks(key, arg, log_writer, hook_data = nil)
295
310
  @options.all_of(key).each do |b|
296
311
  begin
297
- b.call arg
312
+ if Array === b
313
+ hook_data[b[1]] ||= Hash.new
314
+ b[0].call arg, hook_data[b[1]]
315
+ else
316
+ b.call arg
317
+ end
298
318
  rescue => e
299
- events.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
300
- events.debug e.backtrace.join("\n")
319
+ log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
320
+ log_writer.debug e.backtrace.join("\n")
301
321
  end
302
322
  end
303
323
  end
@@ -315,10 +335,6 @@ module Puma
315
335
 
316
336
  private
317
337
 
318
- def infer_tag
319
- File.basename(Dir.getwd)
320
- end
321
-
322
338
  # Load and use the normal Rack builder if we can, otherwise
323
339
  # fallback to our minimal version.
324
340
  def rack_builder
@@ -368,4 +384,4 @@ module Puma
368
384
  end
369
385
  end
370
386
 
371
- require 'puma/dsl'
387
+ require_relative 'dsl'
data/lib/puma/const.rb CHANGED
@@ -100,32 +100,17 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "5.6.4".freeze
104
- CODE_NAME = "Birdie's Version".freeze
103
+ PUMA_VERSION = VERSION = "6.0.0".freeze
104
+ CODE_NAME = "Sunflower".freeze
105
105
 
106
106
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
107
107
 
108
108
  FAST_TRACK_KA_TIMEOUT = 0.2
109
109
 
110
- # The default number of seconds for another request within a persistent
111
- # session.
112
- PERSISTENT_TIMEOUT = 20
113
-
114
- # The default number of seconds to wait until we get the first data
115
- # for the request
116
- FIRST_DATA_TIMEOUT = 30
117
-
118
110
  # How long to wait when getting some write blocking on the socket when
119
111
  # sending data back
120
112
  WRITE_TIMEOUT = 10
121
113
 
122
- # How many requests to attempt inline before sending a client back to
123
- # the reactor to be subject to normal ordering. The idea here is that
124
- # we amortize the cost of going back to the reactor for a well behaved
125
- # but very "greedy" client across 10 requests. This prevents a not
126
- # well behaved client from monopolizing the thread forever.
127
- MAX_FAST_INLINE = 10
128
-
129
114
  # The original URI requested by the client.
130
115
  REQUEST_URI= 'REQUEST_URI'.freeze
131
116
  REQUEST_PATH = 'REQUEST_PATH'.freeze
@@ -163,6 +148,14 @@ module Puma
163
148
 
164
149
  REQUEST_METHOD = "REQUEST_METHOD".freeze
165
150
  HEAD = "HEAD".freeze
151
+ GET = "GET".freeze
152
+ POST = "POST".freeze
153
+ PUT = "PUT".freeze
154
+ DELETE = "DELETE".freeze
155
+ OPTIONS = "OPTIONS".freeze
156
+ TRACE = "TRACE".freeze
157
+ PATCH = "PATCH".freeze
158
+ SUPPORTED_HTTP_METHODS = [HEAD, GET, POST, PUT, DELETE, OPTIONS, TRACE, PATCH].freeze
166
159
  # ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32)
167
160
  LINE_END = "\r\n".freeze
168
161
  REMOTE_ADDR = "REMOTE_ADDR".freeze
@@ -177,7 +170,10 @@ module Puma
177
170
  PORT_80 = "80".freeze
178
171
  PORT_443 = "443".freeze
179
172
  LOCALHOST = "localhost".freeze
180
- LOCALHOST_IP = "127.0.0.1".freeze
173
+ LOCALHOST_IPV4 = "127.0.0.1".freeze
174
+ LOCALHOST_IPV6 = "::1".freeze
175
+ UNSPECIFIED_IPV4 = "0.0.0.0".freeze
176
+ UNSPECIFIED_IPV6 = "::".freeze
181
177
 
182
178
  SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
183
179
  HTTP_11 = "HTTP/1.1".freeze
@@ -17,26 +17,27 @@ module Puma
17
17
  CMD_PATH_SIG_MAP = {
18
18
  'gc' => nil,
19
19
  'gc-stats' => nil,
20
- 'halt' => 'SIGQUIT',
21
- 'phased-restart' => 'SIGUSR1',
22
- 'refork' => 'SIGURG',
20
+ 'halt' => 'SIGQUIT',
21
+ 'info' => 'SIGINFO',
22
+ 'phased-restart' => 'SIGUSR1',
23
+ 'refork' => 'SIGURG',
23
24
  'reload-worker-directory' => nil,
24
- 'restart' => 'SIGUSR2',
25
+ 'reopen-log' => 'SIGHUP',
26
+ 'restart' => 'SIGUSR2',
25
27
  'start' => nil,
26
28
  'stats' => nil,
27
29
  'status' => '',
28
- 'stop' => 'SIGTERM',
29
- 'thread-backtraces' => nil
30
+ 'stop' => 'SIGTERM',
31
+ 'thread-backtraces' => nil,
32
+ 'worker-count-down' => 'SIGTTOU',
33
+ 'worker-count-up' => 'SIGTTIN'
30
34
  }.freeze
31
35
 
32
- # @deprecated 6.0.0
33
- COMMANDS = CMD_PATH_SIG_MAP.keys.freeze
34
-
35
36
  # commands that cannot be used in a request
36
- NO_REQ_COMMANDS = %w{refork}.freeze
37
+ NO_REQ_COMMANDS = %w[info reopen-log worker-count-down worker-count-up].freeze
37
38
 
38
39
  # @version 5.0.0
39
- PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}.freeze
40
+ PRINTABLE_COMMANDS = %w[gc-stats stats thread-backtraces].freeze
40
41
 
41
42
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
42
43
  @state = nil
@@ -185,8 +186,6 @@ module Puma
185
186
 
186
187
  if @command == 'status'
187
188
  message 'Puma is started'
188
- elsif NO_REQ_COMMANDS.include? @command
189
- raise "Invalid request command: #{@command}"
190
189
  else
191
190
  url = "/#{@command}"
192
191
 
@@ -242,7 +241,11 @@ module Puma
242
241
  @stdout.flush unless @stdout.sync
243
242
  return
244
243
  elsif sig.start_with? 'SIG'
245
- Process.kill sig, @pid
244
+ if Signal.list.key? sig.sub(/\ASIG/, '')
245
+ Process.kill sig, @pid
246
+ else
247
+ raise "Signal '#{sig}' not available'"
248
+ end
246
249
  elsif @command == 'status'
247
250
  begin
248
251
  Process.kill 0, @pid
@@ -268,7 +271,7 @@ module Puma
268
271
  return start if @command == 'start'
269
272
  prepare_configuration
270
273
 
271
- if Puma.windows? || @control_url
274
+ if Puma.windows? || @control_url && !NO_REQ_COMMANDS.include?(@command)
272
275
  send_request
273
276
  else
274
277
  send_signal
@@ -281,7 +284,7 @@ module Puma
281
284
 
282
285
  private
283
286
  def start
284
- require 'puma/cli'
287
+ require_relative 'cli'
285
288
 
286
289
  run_args = []
287
290
 
@@ -293,13 +296,13 @@ module Puma
293
296
  run_args += ["-C", @config_file] if @config_file
294
297
  run_args += ["-e", @environment] if @environment
295
298
 
296
- events = Puma::Events.new @stdout, @stderr
299
+ log_writer = Puma::LogWriter.new(@stdout, @stderr)
297
300
 
298
301
  # replace $0 because puma use it to generate restart command
299
302
  puma_cmd = $0.gsub(/pumactl$/, 'puma')
300
303
  $0 = puma_cmd if File.exist?(puma_cmd)
301
304
 
302
- cli = Puma::CLI.new run_args, events
305
+ cli = Puma::CLI.new run_args, log_writer
303
306
  cli.run
304
307
  end
305
308
  end
data/lib/puma/detect.rb CHANGED
@@ -8,6 +8,8 @@ module Puma
8
8
  # @version 5.2.1
9
9
  HAS_FORK = ::Process.respond_to? :fork
10
10
 
11
+ HAS_NATIVE_IO_WAIT = ::IO.public_instance_methods(false).include? :wait_readable
12
+
11
13
  IS_JRUBY = Object.const_defined? :JRUBY_VERSION
12
14
 
13
15
  IS_OSX = RUBY_PLATFORM.include? 'darwin'