nio4r 1.2.1-java → 2.0.0-java
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.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop.yml +31 -38
- data/.ruby-version +1 -0
- data/.travis.yml +15 -14
- data/CHANGES.md +75 -42
- data/Gemfile +10 -5
- data/Guardfile +10 -0
- data/LICENSE.txt +1 -1
- data/README.md +57 -161
- data/Rakefile +2 -1
- data/examples/echo_server.rb +1 -0
- data/ext/libev/Changes +4 -13
- data/ext/libev/ev.c +101 -74
- data/ext/libev/ev.h +3 -3
- data/ext/libev/ev_epoll.c +6 -3
- data/ext/libev/ev_kqueue.c +8 -4
- data/ext/libev/ev_poll.c +6 -3
- data/ext/libev/ev_port.c +8 -4
- data/ext/libev/ev_select.c +4 -2
- data/ext/nio4r/bytebuffer.c +421 -0
- data/ext/nio4r/extconf.rb +2 -10
- data/ext/nio4r/monitor.c +93 -46
- data/ext/nio4r/nio4r.h +11 -13
- data/ext/nio4r/org/nio4r/ByteBuffer.java +295 -0
- data/ext/nio4r/org/nio4r/Monitor.java +164 -0
- data/ext/nio4r/org/nio4r/Nio4r.java +22 -391
- data/ext/nio4r/org/nio4r/Selector.java +278 -0
- data/ext/nio4r/selector.c +55 -53
- data/lib/nio.rb +4 -3
- data/lib/nio/bytebuffer.rb +222 -0
- data/lib/nio/monitor.rb +64 -4
- data/lib/nio/selector.rb +52 -20
- data/lib/nio/version.rb +1 -1
- data/nio4r.gemspec +25 -19
- data/spec/nio/acceptables_spec.rb +6 -4
- data/spec/nio/bytebuffer_spec.rb +349 -0
- data/spec/nio/monitor_spec.rb +122 -79
- data/spec/nio/selectables/pipe_spec.rb +5 -1
- data/spec/nio/selectables/ssl_socket_spec.rb +15 -12
- data/spec/nio/selectables/tcp_socket_spec.rb +42 -31
- data/spec/nio/selectables/udp_socket_spec.rb +2 -0
- data/spec/nio/selector_spec.rb +10 -4
- data/spec/spec_helper.rb +24 -3
- data/spec/support/selectable_examples.rb +7 -5
- data/tasks/extension.rake +2 -0
- data/tasks/rspec.rake +2 -0
- data/tasks/rubocop.rake +2 -0
- metadata +21 -14
- 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
|
-
|
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"
|
30
|
+
NIO::ENGINE = "java"
|
30
31
|
else
|
31
|
-
NIO::ENGINE = "libev"
|
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
|
-
|
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
|
-
#
|
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
|
-
|
28
|
-
|
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
|
-
|
41
|
+
raise IOError, "selector is closed" if closed?
|
24
42
|
|
25
43
|
monitor = @selectables[io]
|
26
|
-
|
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
|
62
|
-
return unless ready_readers # timeout
|
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 =
|
98
|
+
monitor.readiness = monitor.readiness == :r ? :rw : :w
|
82
99
|
selected_monitors << monitor
|
83
100
|
end
|
101
|
+
end
|
84
102
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
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
|
-
|
114
|
-
|
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
|