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.
- 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
|