nio4r 1.2.1 → 2.5.3

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 (57) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/workflow.yml +43 -0
  3. data/.gitignore +1 -0
  4. data/.rspec +0 -1
  5. data/.rubocop.yml +70 -31
  6. data/CHANGES.md +190 -42
  7. data/Gemfile +8 -4
  8. data/Guardfile +10 -0
  9. data/README.md +102 -147
  10. data/Rakefile +3 -4
  11. data/examples/echo_server.rb +3 -2
  12. data/ext/libev/Changes +44 -13
  13. data/ext/libev/README +2 -1
  14. data/ext/libev/ev.c +314 -225
  15. data/ext/libev/ev.h +90 -88
  16. data/ext/libev/ev_epoll.c +30 -16
  17. data/ext/libev/ev_kqueue.c +19 -9
  18. data/ext/libev/ev_linuxaio.c +642 -0
  19. data/ext/libev/ev_poll.c +19 -11
  20. data/ext/libev/ev_port.c +13 -6
  21. data/ext/libev/ev_select.c +4 -2
  22. data/ext/libev/ev_vars.h +14 -3
  23. data/ext/libev/ev_wrap.h +16 -0
  24. data/ext/nio4r/bytebuffer.c +429 -0
  25. data/ext/nio4r/extconf.rb +17 -30
  26. data/ext/nio4r/monitor.c +113 -49
  27. data/ext/nio4r/nio4r.h +11 -13
  28. data/ext/nio4r/org/nio4r/ByteBuffer.java +293 -0
  29. data/ext/nio4r/org/nio4r/Monitor.java +175 -0
  30. data/ext/nio4r/org/nio4r/Nio4r.java +22 -391
  31. data/ext/nio4r/org/nio4r/Selector.java +299 -0
  32. data/ext/nio4r/selector.c +155 -68
  33. data/lib/nio.rb +4 -4
  34. data/lib/nio/bytebuffer.rb +229 -0
  35. data/lib/nio/monitor.rb +73 -11
  36. data/lib/nio/selector.rb +64 -21
  37. data/lib/nio/version.rb +1 -1
  38. data/nio4r.gemspec +34 -20
  39. data/{tasks → rakelib}/extension.rake +4 -0
  40. data/{tasks → rakelib}/rspec.rake +2 -0
  41. data/{tasks → rakelib}/rubocop.rake +2 -0
  42. data/spec/nio/acceptables_spec.rb +5 -5
  43. data/spec/nio/bytebuffer_spec.rb +354 -0
  44. data/spec/nio/monitor_spec.rb +128 -79
  45. data/spec/nio/selectables/pipe_spec.rb +12 -3
  46. data/spec/nio/selectables/ssl_socket_spec.rb +61 -29
  47. data/spec/nio/selectables/tcp_socket_spec.rb +47 -34
  48. data/spec/nio/selectables/udp_socket_spec.rb +24 -7
  49. data/spec/nio/selector_spec.rb +65 -16
  50. data/spec/spec_helper.rb +12 -3
  51. data/spec/support/selectable_examples.rb +45 -18
  52. metadata +33 -23
  53. data/.rubocop_todo.yml +0 -35
  54. data/.travis.yml +0 -27
  55. data/LICENSE.txt +0 -20
  56. data/ext/libev/README.embed +0 -3
  57. data/ext/libev/test_libev_win32.c +0 -123
data/lib/nio.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
3
  require "socket"
5
4
  require "nio/version"
6
5
 
@@ -18,7 +17,8 @@ end
18
17
  if ENV["NIO4R_PURE"] == "true" || (Gem.win_platform? && !defined?(JRUBY_VERSION))
19
18
  require "nio/monitor"
20
19
  require "nio/selector"
21
- NIO::ENGINE = "ruby".freeze
20
+ require "nio/bytebuffer"
21
+ NIO::ENGINE = "ruby"
22
22
  else
23
23
  require "nio4r_ext"
24
24
 
@@ -26,8 +26,8 @@ else
26
26
  require "java"
27
27
  require "jruby"
28
28
  org.nio4r.Nio4r.new.load(JRuby.runtime, false)
29
- NIO::ENGINE = "java".freeze
29
+ NIO::ENGINE = "java"
30
30
  else
31
- NIO::ENGINE = "libev".freeze
31
+ NIO::ENGINE = "libev"
32
32
  end
33
33
  end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NIO
4
+ # Efficient byte buffers for performant I/O operations
5
+ class ByteBuffer
6
+ include Enumerable
7
+
8
+ attr_reader :position, :limit, :capacity
9
+
10
+ # Insufficient capacity in buffer
11
+ OverflowError = Class.new(IOError)
12
+
13
+ # Not enough data remaining in buffer
14
+ UnderflowError = Class.new(IOError)
15
+
16
+ # Mark has not been set
17
+ MarkUnsetError = Class.new(IOError)
18
+
19
+ # Create a new ByteBuffer, either with a specified capacity or populating
20
+ # it from a given string
21
+ #
22
+ # @param capacity [Integer] size of buffer in bytes
23
+ #
24
+ # @return [NIO::ByteBuffer]
25
+ def initialize(capacity)
26
+ raise TypeError, "no implicit conversion of #{capacity.class} to Integer" unless capacity.is_a?(Integer)
27
+
28
+ @capacity = capacity
29
+ clear
30
+ end
31
+
32
+ # Clear the buffer, resetting it to the default state
33
+ def clear
34
+ @buffer = ("\0" * @capacity).force_encoding(Encoding::BINARY)
35
+ @position = 0
36
+ @limit = @capacity
37
+ @mark = nil
38
+
39
+ self
40
+ end
41
+
42
+ # Set the position to the given value. New position must be less than limit.
43
+ # Preserves mark if it's less than the new position, otherwise clears it.
44
+ #
45
+ # @param new_position [Integer] position in the buffer
46
+ #
47
+ # @raise [ArgumentError] new position was invalid
48
+ def position=(new_position)
49
+ raise ArgumentError, "negative position given" if new_position < 0
50
+ raise ArgumentError, "specified position exceeds capacity" if new_position > @capacity
51
+
52
+ @mark = nil if @mark && @mark > new_position
53
+ @position = new_position
54
+ end
55
+
56
+ # Set the limit to the given value. New limit must be less than capacity.
57
+ # Preserves limit and mark if they're less than the new limit, otherwise
58
+ # sets position to the new limit and clears the mark.
59
+ #
60
+ # @param new_limit [Integer] position in the buffer
61
+ #
62
+ # @raise [ArgumentError] new limit was invalid
63
+ def limit=(new_limit)
64
+ raise ArgumentError, "negative limit given" if new_limit < 0
65
+ raise ArgumentError, "specified limit exceeds capacity" if new_limit > @capacity
66
+
67
+ @position = new_limit if @position > new_limit
68
+ @mark = nil if @mark && @mark > new_limit
69
+ @limit = new_limit
70
+ end
71
+
72
+ # Number of bytes remaining in the buffer before the limit
73
+ #
74
+ # @return [Integer] number of bytes remaining
75
+ def remaining
76
+ @limit - @position
77
+ end
78
+
79
+ # Does the ByteBuffer have any space remaining?
80
+ #
81
+ # @return [true, false]
82
+ def full?
83
+ remaining.zero?
84
+ end
85
+
86
+ # Obtain the requested number of bytes from the buffer, advancing the position.
87
+ # If no length is given, all remaining bytes are consumed.
88
+ #
89
+ # @raise [NIO::ByteBuffer::UnderflowError] not enough data remaining in buffer
90
+ #
91
+ # @return [String] bytes read from buffer
92
+ def get(length = remaining)
93
+ raise ArgumentError, "negative length given" if length < 0
94
+ raise UnderflowError, "not enough data in buffer" if length > @limit - @position
95
+
96
+ result = @buffer[@position...length]
97
+ @position += length
98
+ result
99
+ end
100
+
101
+ # Obtain the byte at a given index in the buffer as an Integer
102
+ #
103
+ # @raise [ArgumentError] index is invalid (either negative or larger than limit)
104
+ #
105
+ # @return [Integer] byte at the given index
106
+ def [](index)
107
+ raise ArgumentError, "negative index given" if index < 0
108
+ raise ArgumentError, "specified index exceeds limit" if index >= @limit
109
+
110
+ @buffer.bytes[index]
111
+ end
112
+
113
+ # Add a String to the buffer
114
+ #
115
+ # @param str [#to_str] data to add to the buffer
116
+ #
117
+ # @raise [TypeError] given a non-string type
118
+ # @raise [NIO::ByteBuffer::OverflowError] buffer is full
119
+ #
120
+ # @return [self]
121
+ def put(str)
122
+ raise TypeError, "expected String, got #{str.class}" unless str.respond_to?(:to_str)
123
+
124
+ str = str.to_str
125
+
126
+ raise OverflowError, "buffer is full" if str.length > @limit - @position
127
+
128
+ @buffer[@position...str.length] = str
129
+ @position += str.length
130
+ self
131
+ end
132
+ alias << put
133
+
134
+ # Perform a non-blocking read from the given IO object into the buffer
135
+ # Reads as much data as is immediately available and returns
136
+ #
137
+ # @param [IO] Ruby IO object to read from
138
+ #
139
+ # @return [Integer] number of bytes read (0 if none were available)
140
+ def read_from(io)
141
+ nbytes = @limit - @position
142
+ raise OverflowError, "buffer is full" if nbytes.zero?
143
+
144
+ bytes_read = IO.try_convert(io).read_nonblock(nbytes, exception: false)
145
+ return 0 if bytes_read == :wait_readable
146
+
147
+ self << bytes_read
148
+ bytes_read.length
149
+ end
150
+
151
+ # Perform a non-blocking write of the buffer's contents to the given I/O object
152
+ # Writes as much data as is immediately possible and returns
153
+ #
154
+ # @param [IO] Ruby IO object to write to
155
+ #
156
+ # @return [Integer] number of bytes written (0 if the write would block)
157
+ def write_to(io)
158
+ nbytes = @limit - @position
159
+ raise UnderflowError, "no data remaining in buffer" if nbytes.zero?
160
+
161
+ bytes_written = IO.try_convert(io).write_nonblock(@buffer[@position...@limit], exception: false)
162
+ return 0 if bytes_written == :wait_writable
163
+
164
+ @position += bytes_written
165
+ bytes_written
166
+ end
167
+
168
+ # Set the buffer's current position as the limit and set the position to 0
169
+ def flip
170
+ @limit = @position
171
+ @position = 0
172
+ @mark = nil
173
+ self
174
+ end
175
+
176
+ # Set the buffer's current position to 0, leaving the limit unchanged
177
+ def rewind
178
+ @position = 0
179
+ @mark = nil
180
+ self
181
+ end
182
+
183
+ # Mark a position to return to using the `#reset` method
184
+ def mark
185
+ @mark = @position
186
+ self
187
+ end
188
+
189
+ # Reset position to the previously marked location
190
+ #
191
+ # @raise [NIO::ByteBuffer::MarkUnsetError] mark has not been set (call `#mark` first)
192
+ def reset
193
+ raise MarkUnsetError, "mark has not been set" unless @mark
194
+
195
+ @position = @mark
196
+ self
197
+ end
198
+
199
+ # Move data between the position and limit to the beginning of the buffer
200
+ # Sets the position to the end of the moved data, and the limit to the capacity
201
+ def compact
202
+ @buffer[0...(@limit - @position)] = @buffer[@position...@limit]
203
+ @position = @limit - @position
204
+ @limit = capacity
205
+ self
206
+ end
207
+
208
+ # Iterate over the bytes in the buffer (as Integers)
209
+ #
210
+ # @return [self]
211
+ def each(&block)
212
+ @buffer[0...@limit].each_byte(&block)
213
+ end
214
+
215
+ # Inspect the state of the buffer
216
+ #
217
+ # @return [String] string describing the state of the buffer
218
+ def inspect
219
+ format(
220
+ "#<%s:0x%x @position=%d @limit=%d @capacity=%d>",
221
+ self.class,
222
+ object_id << 1,
223
+ @position,
224
+ @limit,
225
+ @capacity
226
+ )
227
+ end
228
+ end
229
+ end
@@ -1,19 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NIO
2
4
  # Monitors watch IO objects for specific events
3
5
  class Monitor
4
6
  attr_reader :io, :interests, :selector
5
7
  attr_accessor :value, :readiness
6
8
 
7
- # :nodoc
9
+ # :nodoc:
8
10
  def initialize(io, interests, selector)
9
- unless io.is_a?(IO)
10
- if IO.respond_to? :try_convert
11
- io = IO.try_convert(io)
12
- elsif io.respond_to? :to_io
13
- io = io.to_io
14
- end
11
+ unless defined?(::OpenSSL) && io.is_a?(::OpenSSL::SSL::SSLSocket)
12
+ unless io.is_a?(IO)
13
+ if IO.respond_to? :try_convert
14
+ io = IO.try_convert(io)
15
+ elsif io.respond_to? :to_io
16
+ io = io.to_io
17
+ end
15
18
 
16
- fail TypeError, "can't convert #{io.class} into IO" unless io.is_a? IO
19
+ raise TypeError, "can't convert #{io.class} into IO" unless io.is_a? IO
20
+ end
17
21
  end
18
22
 
19
23
  @io = io
@@ -22,14 +26,72 @@ module NIO
22
26
  @closed = false
23
27
  end
24
28
 
25
- # set the interests set
29
+ # Replace the existing interest set with a new one
30
+ #
31
+ # @param interests [:r, :w, :rw, nil] I/O readiness we're interested in (read/write/readwrite)
32
+ #
33
+ # @return [Symbol] new interests
26
34
  def interests=(interests)
27
- fail TypeError, "monitor is already closed" if closed?
28
- fail ArgumentError, "bad interests: #{interests}" unless [:r, :w, :rw].include?(interests)
35
+ raise EOFError, "monitor is closed" if closed?
36
+ raise ArgumentError, "bad interests: #{interests}" unless [:r, :w, :rw, nil].include?(interests)
29
37
 
30
38
  @interests = interests
31
39
  end
32
40
 
41
+ # Add new interests to the existing interest set
42
+ #
43
+ # @param interests [:r, :w, :rw] new I/O interests (read/write/readwrite)
44
+ #
45
+ # @return [self]
46
+ def add_interest(interest)
47
+ case interest
48
+ when :r
49
+ case @interests
50
+ when :r then @interests = :r
51
+ when :w then @interests = :rw
52
+ when :rw then @interests = :rw
53
+ when nil then @interests = :r
54
+ end
55
+ when :w
56
+ case @interests
57
+ when :r then @interests = :rw
58
+ when :w then @interests = :w
59
+ when :rw then @interests = :rw
60
+ when nil then @interests = :w
61
+ end
62
+ when :rw
63
+ @interests = :rw
64
+ else raise ArgumentError, "bad interests: #{interest}"
65
+ end
66
+ end
67
+
68
+ # Remove interests from the existing interest set
69
+ #
70
+ # @param interests [:r, :w, :rw] I/O interests to remove (read/write/readwrite)
71
+ #
72
+ # @return [self]
73
+ def remove_interest(interest)
74
+ case interest
75
+ when :r
76
+ case @interests
77
+ when :r then @interests = nil
78
+ when :w then @interests = :w
79
+ when :rw then @interests = :w
80
+ when nil then @interests = nil
81
+ end
82
+ when :w
83
+ case @interests
84
+ when :r then @interests = :r
85
+ when :w then @interests = nil
86
+ when :rw then @interests = :r
87
+ when nil then @interests = nil
88
+ end
89
+ when :rw
90
+ @interests = nil
91
+ else raise ArgumentError, "bad interests: #{interest}"
92
+ end
93
+ end
94
+
33
95
  # Is the IO object readable?
34
96
  def readable?
35
97
  readiness == :r || readiness == :rw
@@ -1,10 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "set"
2
4
 
3
5
  module NIO
4
6
  # Selectors monitor IO objects for events of interest
5
7
  class Selector
8
+ # Return supported backends as symbols
9
+ #
10
+ # See `#backend` method definition for all possible backends
11
+ def self.backends
12
+ [:ruby]
13
+ end
14
+
6
15
  # Create a new NIO::Selector
7
- def initialize
16
+ def initialize(backend = :ruby)
17
+ raise ArgumentError, "unsupported backend: #{backend}" unless backend == :ruby
18
+
8
19
  @selectables = {}
9
20
  @lock = Mutex.new
10
21
 
@@ -13,17 +24,35 @@ module NIO
13
24
  @closed = false
14
25
  end
15
26
 
27
+ # Return a symbol representing the backend I/O multiplexing mechanism used.
28
+ # Supported backends are:
29
+ # * :ruby - pure Ruby (i.e IO.select)
30
+ # * :java - Java NIO on JRuby
31
+ # * :epoll - libev w\ Linux epoll
32
+ # * :poll - libev w\ POSIX poll
33
+ # * :kqueue - libev w\ BSD kqueue
34
+ # * :select - libev w\ SysV select
35
+ # * :port - libev w\ I/O completion ports
36
+ # * :unknown - libev w\ unknown backend
37
+ def backend
38
+ :ruby
39
+ end
40
+
16
41
  # Register interest in an IO object with the selector for the given types
17
42
  # of events. Valid event types for interest are:
18
43
  # * :r - is the IO readable?
19
44
  # * :w - is the IO writeable?
20
45
  # * :rw - is the IO either readable or writeable?
21
46
  def register(io, interest)
47
+ unless defined?(::OpenSSL) && io.is_a?(::OpenSSL::SSL::SSLSocket)
48
+ io = IO.try_convert(io)
49
+ end
50
+
22
51
  @lock.synchronize do
23
- fail IOError, "selector is closed" if closed?
52
+ raise IOError, "selector is closed" if closed?
24
53
 
25
54
  monitor = @selectables[io]
26
- fail ArgumentError, "already registered as #{monitor.interests.inspect}" if monitor
55
+ raise ArgumentError, "already registered as #{monitor.interests.inspect}" if monitor
27
56
 
28
57
  monitor = Monitor.new(io, interest, self)
29
58
  @selectables[monitor.io] = monitor
@@ -35,7 +64,7 @@ module NIO
35
64
  # Deregister the given IO object from the selector
36
65
  def deregister(io)
37
66
  @lock.synchronize do
38
- monitor = @selectables.delete io
67
+ monitor = @selectables.delete IO.try_convert(io)
39
68
  monitor.close(false) if monitor && !monitor.closed?
40
69
  monitor
41
70
  end
@@ -48,6 +77,8 @@ module NIO
48
77
 
49
78
  # Select which monitors are ready
50
79
  def select(timeout = nil)
80
+ selected_monitors = Set.new
81
+
51
82
  @lock.synchronize do
52
83
  readers = [@wakeup]
53
84
  writers = []
@@ -58,17 +89,14 @@ module NIO
58
89
  monitor.readiness = nil
59
90
  end
60
91
 
61
- ready_readers, ready_writers = Kernel.select readers, writers, [], timeout
62
- return unless ready_readers # timeout or wakeup
63
-
64
- selected_monitors = Set.new
92
+ ready_readers, ready_writers = Kernel.select(readers, writers, [], timeout)
93
+ return unless ready_readers # timeout
65
94
 
66
95
  ready_readers.each do |io|
67
96
  if io == @wakeup
68
97
  # Clear all wakeup signals we've received by reading them
69
98
  # Wakeups should have level triggered behavior
70
99
  @wakeup.read(@wakeup.stat.size)
71
- return
72
100
  else
73
101
  monitor = @selectables[io]
74
102
  monitor.readiness = :r
@@ -78,18 +106,16 @@ module NIO
78
106
 
79
107
  ready_writers.each do |io|
80
108
  monitor = @selectables[io]
81
- monitor.readiness = (monitor.readiness == :r) ? :rw : :w
109
+ monitor.readiness = monitor.readiness == :r ? :rw : :w
82
110
  selected_monitors << monitor
83
111
  end
112
+ end
84
113
 
85
- if block_given?
86
- selected_monitors.each do |m|
87
- yield m
88
- end
89
- selected_monitors.size
90
- else
91
- selected_monitors
92
- end
114
+ if block_given?
115
+ selected_monitors.each { |m| yield m }
116
+ selected_monitors.size
117
+ else
118
+ selected_monitors.to_a
93
119
  end
94
120
  end
95
121
 
@@ -101,7 +127,16 @@ module NIO
101
127
  # level-triggered behavior.
102
128
  def wakeup
103
129
  # Send the selector a signal in the form of writing data to a pipe
104
- @waker.write "\0"
130
+ begin
131
+ @waker.write_nonblock "\0"
132
+ rescue IO::WaitWritable
133
+ # This indicates the wakeup pipe is full, which means the other thread
134
+ # has already received many wakeup calls, but not processed them yet.
135
+ # The other thread will completely drain this pipe when it wakes up,
136
+ # so it's ok to ignore this exception if it occurs: we know the other
137
+ # thread has already been signaled to wake up
138
+ end
139
+
105
140
  nil
106
141
  end
107
142
 
@@ -110,8 +145,16 @@ module NIO
110
145
  @lock.synchronize do
111
146
  return if @closed
112
147
 
113
- @wakeup.close rescue nil
114
- @waker.close rescue nil
148
+ begin
149
+ @wakeup.close
150
+ rescue IOError
151
+ end
152
+
153
+ begin
154
+ @waker.close
155
+ rescue IOError
156
+ end
157
+
115
158
  @closed = true
116
159
  end
117
160
  end