nio4r 1.2.1-java → 2.0.0-java

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