nio4r 1.2.1 → 2.5.3

Sign up to get free protection for your applications and to get access to all the features.
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