nio4r 2.0.0.pre → 2.0.0

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 +4 -21
  6. data/CHANGES.md +75 -42
  7. data/Gemfile +11 -3
  8. data/Guardfile +10 -0
  9. data/LICENSE.txt +1 -1
  10. data/README.md +32 -136
  11. data/Rakefile +2 -0
  12. data/examples/echo_server.rb +1 -0
  13. data/ext/libev/Changes +4 -13
  14. data/ext/libev/ev.c +100 -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 +265 -257
  22. data/ext/nio4r/extconf.rb +2 -10
  23. data/ext/nio4r/monitor.c +93 -46
  24. data/ext/nio4r/nio4r.h +5 -15
  25. data/ext/nio4r/org/nio4r/ByteBuffer.java +193 -209
  26. data/ext/nio4r/org/nio4r/Monitor.java +164 -0
  27. data/ext/nio4r/org/nio4r/Nio4r.java +13 -391
  28. data/ext/nio4r/org/nio4r/Selector.java +278 -0
  29. data/ext/nio4r/selector.c +52 -52
  30. data/lib/nio.rb +3 -3
  31. data/lib/nio/bytebuffer.rb +179 -132
  32. data/lib/nio/monitor.rb +64 -4
  33. data/lib/nio/selector.rb +36 -13
  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 +323 -51
  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 +15 -11
  50. data/.rubocop_todo.yml +0 -35
data/lib/nio.rb CHANGED
@@ -19,7 +19,7 @@ if ENV["NIO4R_PURE"] == "true" || (Gem.win_platform? && !defined?(JRUBY_VERSION)
19
19
  require "nio/monitor"
20
20
  require "nio/selector"
21
21
  require "nio/bytebuffer"
22
- NIO::ENGINE = "ruby".freeze
22
+ NIO::ENGINE = "ruby"
23
23
  else
24
24
  require "nio4r_ext"
25
25
 
@@ -27,8 +27,8 @@ else
27
27
  require "java"
28
28
  require "jruby"
29
29
  org.nio4r.Nio4r.new.load(JRuby.runtime, false)
30
- NIO::ENGINE = "java".freeze
30
+ NIO::ENGINE = "java"
31
31
  else
32
- NIO::ENGINE = "libev".freeze
32
+ NIO::ENGINE = "libev"
33
33
  end
34
34
  end
@@ -1,175 +1,222 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NIO
2
- # rubocop:disable ClassLength
4
+ # Efficient byte buffers for performant I/O operations
3
5
  class ByteBuffer
4
- def initialize(value, offset = nil, length = nil)
5
- # value can be either STRING or INTEGER
6
- fail "not a valid input" if value.nil?
7
- @position = 0
8
- @mark = -1
9
- if value.is_a? Integer
10
- @size = value
11
- @byte_array = Array.new(value)
12
- elsif value.is_a? String
13
- @byte_array = str.bytes
14
- @size = @byte_array.size
15
- end
16
- @limit = @size - 1
17
- unless offset.nil?
18
- @offset = offset
19
- @position = offset
20
- unless length.nil?
21
- fail "Invalid Arguiments Exception" if offset + length >= value
22
- @limit = offset + length
23
- end
24
- end
25
- end
26
-
27
- # put the provided string to the buffer
28
- def <<(str)
29
- temp_buffer = str.bytes
30
- temp_buffer.each { |x| put_byte x }
31
- end
6
+ include Enumerable
32
7
 
33
- # return the remaining number positions to read/ write
34
- def remaining
35
- @limit + 1 - @position
36
- end
8
+ attr_reader :position, :limit, :capacity
37
9
 
38
- # has any space remaining
39
- def remaining?
40
- remaining > 0
41
- end
10
+ # Insufficient capacity in buffer
11
+ OverflowError = Class.new(IOError)
12
+
13
+ # Not enough data remaining in buffer
14
+ UnderflowError = Class.new(IOError)
42
15
 
43
- # this method is private
44
- def put_byte(byte)
45
- fail "Buffer Overflowed" if @position == @size
46
- @byte_array[@position] = byte
47
- @position += 1
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
48
29
  end
49
30
 
50
- # write content in the buffer to file
51
- # call flip before calling this
52
- # after write operation to the
53
- # buffer
54
- def write_to(file)
55
- @file_to_write = file unless @file_to_write.eql? file
56
- file.write get if remaining?
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
57
39
  end
58
40
 
59
- # Fill the byteBuffer with content of the file
60
- def read_from(file)
61
- @file_to_read = file unless @file_to_read.eql? file
62
- while (s = file.read(1)) && remaining?
63
- put_byte(s)
64
- end
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
65
55
  end
66
56
 
67
- # flip from write to read mode
68
- def flip
69
- # need to avoid @position being negative
70
- @limit = [@position - 1, 0].max
71
- @position = 0
72
- @mark = -1
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
73
  end
74
74
 
75
- # rewind read mode to write mode. limit stays unchanged
76
- def rewind
77
- @position = 0
78
- @mark = -1
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
79
80
  end
80
81
 
81
- # reset the position to the previously marked position
82
- def reset
83
- fail "Invalid Mark Exception" if @mark < 0
84
- @position = @mark
85
- self
82
+ # Does the ByteBuffer have any space remaining?
83
+ #
84
+ # @return [true, false]
85
+ def full?
86
+ remaining.zero?
86
87
  end
87
88
 
88
- # mark the current position in order to reset later
89
- def mark
90
- @mark = @position
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
91
102
  end
92
103
 
93
- # the current values are considered junk
94
- def clear
95
- @position = 0
96
- @limit = @size - 1
97
- @mark = -1
98
- self
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]
99
114
  end
100
115
 
101
- def compact
102
- # compact should be allowed only if there are content remaining in the buffer
103
- return self unless remaining?
104
- temp = @byte_array.slice(@position, @limit)
105
- # if 1 remaining the replaced range should be @byte_array[0..0]
106
- @byte_array[0..remaining - 1] = temp
107
- @position = remaining
108
- @limit = @size - 1
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
109
125
  self
110
126
  end
111
127
 
112
- # get the content of the byteBuffer. need to call rewind before calling get.
113
- # return as a String
114
- def get
115
- return "" if @limit == 0
116
- temp = @byte_array[@position..@limit].pack("c*")
117
- # next position to be read. it should be always less than or equal to size-1
118
- @position = [@limit + 1, @size].min
119
- temp
120
- end
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
121
140
 
122
- def read_next(count)
123
- fail "Illegal Argument" unless count > 0
124
- fail "Less number of elements remaining" if count > remaining
125
- temp = @byte_array[@position..@position + count - 1].pack("c*")
126
- @position += count
127
- temp
141
+ self << bytes_read
142
+ bytes_read.length
128
143
  end
129
144
 
130
- # return the offset of the buffer
131
- def offset?
132
- @offset
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
133
160
  end
134
161
 
135
- # check whether the obj is the same bytebuffer as this bytebuffer
136
- def equals?(obj)
137
- self == obj
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
138
168
  end
139
169
 
140
- # returns the capacity of the buffer. This value is fixed to the initial size
141
- def capacity
142
- @size
170
+ # Set the buffer's current position to 0, leaving the limit unchanged
171
+ def rewind
172
+ @position = 0
173
+ @mark = nil
174
+ self
143
175
  end
144
176
 
145
- # Set the position to a different position
146
- def position(new_position)
147
- fail "Illegal Argument Exception" unless new_position <= @limit && new_position >= 0
148
- @position = new_position
149
- @mark = -1 if @mark > @position
177
+ # Mark a position to return to using the `#reset` method
178
+ def mark
179
+ @mark = @position
180
+ self
150
181
  end
151
182
 
152
- def limit(new_limit)
153
- fail "Illegal Argument Exception" if new_limit > @size || new_limit < 0
154
- @limit = new_limit
155
- @position = @limit if @position > @limit
156
- @mark = -1 if @mark > @limit
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
157
190
  end
158
191
 
159
- def limit?
160
- @limit
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
161
199
  end
162
200
 
163
- def to_s
164
- # convert String in byte form to the visible string
165
- temp = "ByteBuffer "
166
- temp += "[pos=" + @position.to_s
167
- temp += " lim =" + @limit.to_s
168
- temp += " cap=" + @size.to_s
169
- temp += "]"
170
- temp
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)
171
206
  end
172
207
 
173
- private :put_byte
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
174
221
  end
175
222
  end
@@ -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