nio4r 0.0.1
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.
- data/.gitignore +20 -0
- data/.rspec +4 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +123 -0
- data/Rakefile +9 -0
- data/ext/libev/Changes +388 -0
- data/ext/libev/LICENSE +36 -0
- data/ext/libev/README +58 -0
- data/ext/libev/README.embed +3 -0
- data/ext/libev/ev.c +3913 -0
- data/ext/libev/ev.h +829 -0
- data/ext/libev/ev_epoll.c +266 -0
- data/ext/libev/ev_kqueue.c +198 -0
- data/ext/libev/ev_poll.c +148 -0
- data/ext/libev/ev_port.c +179 -0
- data/ext/libev/ev_select.c +310 -0
- data/ext/libev/ev_vars.h +203 -0
- data/ext/libev/ev_win32.c +153 -0
- data/ext/libev/ev_wrap.h +196 -0
- data/ext/libev/test_libev_win32.c +123 -0
- data/ext/nio4r/extconf.rb +44 -0
- data/ext/nio4r/libev.h +8 -0
- data/ext/nio4r/monitor.c +164 -0
- data/ext/nio4r/nio4r.h +53 -0
- data/ext/nio4r/selector.c +370 -0
- data/lib/nio.rb +30 -0
- data/lib/nio/jruby/monitor.rb +26 -0
- data/lib/nio/jruby/selector.rb +110 -0
- data/lib/nio/monitor.rb +21 -0
- data/lib/nio/selector.rb +101 -0
- data/lib/nio/version.rb +3 -0
- data/nio4r.gemspec +21 -0
- data/spec/nio/monitor_spec.rb +36 -0
- data/spec/nio/selector_spec.rb +197 -0
- data/spec/spec_helper.rb +3 -0
- data/tasks/extension.rake +10 -0
- data/tasks/rspec.rake +7 -0
- metadata +122 -0
data/lib/nio.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'nio/version'
|
3
|
+
|
4
|
+
# New I/O for Ruby
|
5
|
+
module NIO
|
6
|
+
# NIO implementation, one of the following (as a string):
|
7
|
+
# * select: in pure Ruby using Kernel.select
|
8
|
+
# * libev: as a C extension using libev
|
9
|
+
# * java: using Java NIO
|
10
|
+
def self.engine; ENGINE end
|
11
|
+
end
|
12
|
+
|
13
|
+
if ENV["NIO4R_PURE"]
|
14
|
+
require 'nio/monitor'
|
15
|
+
require 'nio/selector'
|
16
|
+
NIO::ENGINE = 'select'
|
17
|
+
else
|
18
|
+
if defined?(JRUBY_VERSION)
|
19
|
+
require 'java'
|
20
|
+
require 'nio/jruby/monitor'
|
21
|
+
require 'nio/jruby/selector'
|
22
|
+
NIO::ENGINE = 'java'
|
23
|
+
else
|
24
|
+
require 'nio4r_ext'
|
25
|
+
NIO::ENGINE = 'libev'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# TIMTOWTDI!!!
|
30
|
+
Nio = NIO
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module NIO
|
2
|
+
# Monitors watch Channels for specific events
|
3
|
+
class Monitor
|
4
|
+
attr_accessor :value
|
5
|
+
|
6
|
+
# :nodoc
|
7
|
+
def initialize(io, selection_key)
|
8
|
+
@io, @key = io, selection_key
|
9
|
+
selection_key.attach self
|
10
|
+
@closed = false
|
11
|
+
end
|
12
|
+
|
13
|
+
# Obtain the interests for this monitor
|
14
|
+
def interests
|
15
|
+
Selector.iops2sym @key.interestOps
|
16
|
+
end
|
17
|
+
|
18
|
+
# Is this monitor closed?
|
19
|
+
def closed?; @closed; end
|
20
|
+
|
21
|
+
# Deactivate this monitor
|
22
|
+
def close
|
23
|
+
@closed = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module NIO
|
2
|
+
# Selectors monitor IO objects for events of interest
|
3
|
+
class Selector
|
4
|
+
java_import "java.nio.channels.Selector"
|
5
|
+
java_import "java.nio.channels.SelectionKey"
|
6
|
+
|
7
|
+
# Convert nio4r interest symbols to Java NIO interest ops
|
8
|
+
def self.sym2iops(interest)
|
9
|
+
case interest
|
10
|
+
when :r
|
11
|
+
interest = SelectionKey::OP_READ
|
12
|
+
when :w
|
13
|
+
interest = SelectionKey::OP_WRITE
|
14
|
+
when :rw
|
15
|
+
interest = SelectionKey::OP_READ | SelectionKey::OP_WRITE
|
16
|
+
else raise ArgumentError, "invalid interest type: #{interest}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Convert Java NIO interest ops to the corresponding Ruby symbols
|
21
|
+
def self.iops2sym(interest_ops)
|
22
|
+
case interest_ops
|
23
|
+
when SelectionKey::OP_READ
|
24
|
+
:r
|
25
|
+
when SelectionKey::OP_WRITE
|
26
|
+
:w
|
27
|
+
when SelectionKey::OP_READ | SelectionKey::OP_WRITE
|
28
|
+
:rw
|
29
|
+
else raise ArgumentError, "unknown interest op combination: 0x#{interest_ops.to_s(16)}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create a new NIO::Selector
|
34
|
+
def initialize
|
35
|
+
@java_selector = Selector.open
|
36
|
+
@select_lock = Mutex.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Register interest in an IO object with the selector for the given types
|
40
|
+
# of events. Valid event types for interest are:
|
41
|
+
# * :r - is the IO readable?
|
42
|
+
# * :w - is the IO writeable?
|
43
|
+
# * :rw - is the IO either readable or writeable?
|
44
|
+
def register(io, interest)
|
45
|
+
java_channel = io.to_channel
|
46
|
+
java_channel.configureBlocking(false)
|
47
|
+
|
48
|
+
interest_ops = self.class.sym2iops(interest)
|
49
|
+
|
50
|
+
begin
|
51
|
+
selector_key = java_channel.register @java_selector, interest_ops
|
52
|
+
rescue NativeException => ex
|
53
|
+
case ex.cause
|
54
|
+
when java.lang.IllegalArgumentException
|
55
|
+
raise ArgumentError, "invalid interest type for #{channel}: #{interest}"
|
56
|
+
else raise
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
NIO::Monitor.new(io, selector_key)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Deregister the given IO object from the selector
|
64
|
+
def deregister(io)
|
65
|
+
key = io.to_channel.keyFor(@java_selector)
|
66
|
+
return unless key
|
67
|
+
|
68
|
+
monitor = key.attachment
|
69
|
+
monitor.close
|
70
|
+
monitor
|
71
|
+
end
|
72
|
+
|
73
|
+
# Is the given IO object registered with the selector?
|
74
|
+
def registered?(io)
|
75
|
+
key = io.to_channel.keyFor(@java_selector)
|
76
|
+
return unless key
|
77
|
+
!key.attachment.closed?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Select which monitors are ready
|
81
|
+
def select(timeout = nil)
|
82
|
+
@select_lock.synchronize do
|
83
|
+
if timeout
|
84
|
+
ready = @java_selector.select(timeout * 1000)
|
85
|
+
else
|
86
|
+
ready = @java_selector.select
|
87
|
+
end
|
88
|
+
|
89
|
+
return unless ready > 0 # timeout or wakeup
|
90
|
+
@java_selector.selectedKeys.map { |key| key.attachment }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Wake up the other thread that's currently blocking on this selector
|
95
|
+
def wakeup
|
96
|
+
@java_selector.wakeup
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# Close this selector
|
101
|
+
def close
|
102
|
+
@java_selector.close
|
103
|
+
end
|
104
|
+
|
105
|
+
# Is this selector closed?
|
106
|
+
def closed?
|
107
|
+
!@java_selector.isOpen
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/nio/monitor.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module NIO
|
2
|
+
# Monitors watch IO objects for specific events
|
3
|
+
class Monitor
|
4
|
+
attr_reader :io, :interests
|
5
|
+
attr_accessor :value
|
6
|
+
|
7
|
+
# :nodoc
|
8
|
+
def initialize(io, interests)
|
9
|
+
@io, @interests = io, interests
|
10
|
+
@closed = false
|
11
|
+
end
|
12
|
+
|
13
|
+
# Is this monitor closed?
|
14
|
+
def closed?; @closed; end
|
15
|
+
|
16
|
+
# Deactivate this monitor
|
17
|
+
def close
|
18
|
+
@closed = true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/nio/selector.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module NIO
|
2
|
+
# Selectors monitor IO objects for events of interest
|
3
|
+
class Selector
|
4
|
+
# Create a new NIO::Selector
|
5
|
+
def initialize
|
6
|
+
@selectables = {}
|
7
|
+
@lock = Mutex.new
|
8
|
+
|
9
|
+
# Other threads can wake up a selector
|
10
|
+
@wakeup, @waker = IO.pipe
|
11
|
+
@closed = false
|
12
|
+
end
|
13
|
+
|
14
|
+
# Register interest in an IO object with the selector for the given types
|
15
|
+
# of events. Valid event types for interest are:
|
16
|
+
# * :r - is the IO readable?
|
17
|
+
# * :w - is the IO writeable?
|
18
|
+
# * :rw - is the IO either readable or writeable?
|
19
|
+
def register(io, interest)
|
20
|
+
@lock.synchronize do
|
21
|
+
raise ArgumentError, "this IO is already registered with the selector" if @selectables[io]
|
22
|
+
|
23
|
+
monitor = Monitor.new(io, interest)
|
24
|
+
@selectables[io] = monitor
|
25
|
+
|
26
|
+
monitor
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Deregister the given IO object from the selector
|
31
|
+
def deregister(io)
|
32
|
+
@lock.synchronize do
|
33
|
+
monitor = @selectables.delete io
|
34
|
+
monitor.close if monitor
|
35
|
+
monitor
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Is the given IO object registered with the selector?
|
40
|
+
def registered?(io)
|
41
|
+
@lock.synchronize { @selectables.has_key? io }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Select which monitors are ready
|
45
|
+
def select(timeout = nil)
|
46
|
+
@lock.synchronize do
|
47
|
+
readers, writers = [@wakeup], []
|
48
|
+
|
49
|
+
@selectables.each do |io, monitor|
|
50
|
+
readers << io if monitor.interests == :r || monitor.interests == :rw
|
51
|
+
writers << io if monitor.interests == :w || monitor.interests == :rw
|
52
|
+
end
|
53
|
+
|
54
|
+
ready_readers, ready_writers = Kernel.select readers, writers, [], timeout
|
55
|
+
return unless ready_readers # timeout or wakeup
|
56
|
+
|
57
|
+
results = ready_readers
|
58
|
+
results.concat ready_writers if ready_writers
|
59
|
+
|
60
|
+
results.map! do |io|
|
61
|
+
if io == @wakeup
|
62
|
+
# Clear all wakeup signals we've received by reading them
|
63
|
+
# Wakeups should have level triggered behavior
|
64
|
+
begin
|
65
|
+
@wakeup.read_nonblock(1024)
|
66
|
+
|
67
|
+
# Loop until we've drained all incoming events
|
68
|
+
redo
|
69
|
+
rescue Errno::EWOULDBLOCK
|
70
|
+
end
|
71
|
+
|
72
|
+
return
|
73
|
+
else
|
74
|
+
@selectables[io]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Wake up other threads waiting on this selector
|
81
|
+
def wakeup
|
82
|
+
# Send the selector a signal in the form of writing data to a pipe
|
83
|
+
@waker << "\0"
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
# Close this selector and free its resources
|
88
|
+
def close
|
89
|
+
@lock.synchronize do
|
90
|
+
return if @closed
|
91
|
+
|
92
|
+
@wakeup.close rescue nil
|
93
|
+
@waker.close rescue nil
|
94
|
+
@closed = true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Is this selector closed?
|
99
|
+
def closed?; @closed end
|
100
|
+
end
|
101
|
+
end
|
data/lib/nio/version.rb
ADDED
data/nio4r.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/nio/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Tony Arcieri"]
|
6
|
+
gem.email = ["tony.arcieri@gmail.com"]
|
7
|
+
gem.description = "New IO for Ruby"
|
8
|
+
gem.summary = "NIO exposes a set of high performance IO operations on sockets, files, and other Ruby IO objects"
|
9
|
+
gem.homepage = "https://github.com/tarcieri/nio4r"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "nio4r"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = NIO::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency "rake-compiler", "~> 0.7.9"
|
19
|
+
gem.add_development_dependency "rake"
|
20
|
+
gem.add_development_dependency "rspec", ">= 2.7.0"
|
21
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NIO::Monitor do
|
4
|
+
let :readable_pipe do
|
5
|
+
pipe, peer = IO.pipe
|
6
|
+
peer << "data"
|
7
|
+
pipe
|
8
|
+
end
|
9
|
+
|
10
|
+
# let :unreadable_pipe do
|
11
|
+
# pipe, _ = IO.pipe
|
12
|
+
# pipe
|
13
|
+
# end
|
14
|
+
|
15
|
+
let :selector do
|
16
|
+
NIO::Selector.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Monitors are created by registering IO objects or channels with a selector
|
20
|
+
subject { selector.register(readable_pipe, :r) }
|
21
|
+
|
22
|
+
it "knows its interests" do
|
23
|
+
subject.interests.should == :r
|
24
|
+
end
|
25
|
+
|
26
|
+
it "stores arbitrary values" do
|
27
|
+
subject.value = 42
|
28
|
+
subject.value.should == 42
|
29
|
+
end
|
30
|
+
|
31
|
+
it "closes" do
|
32
|
+
subject.should_not be_closed
|
33
|
+
subject.close
|
34
|
+
subject.should be_closed
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Timeouts should be at least this precise (in seconds) to pass the tests
|
4
|
+
# Typical precision should be better than this, but if it's worse it will fail
|
5
|
+
# the tests
|
6
|
+
TIMEOUT_PRECISION = 0.1
|
7
|
+
|
8
|
+
describe NIO::Selector do
|
9
|
+
it "monitors IO objects" do
|
10
|
+
pipe, _ = IO.pipe
|
11
|
+
|
12
|
+
monitor = subject.register(pipe, :r)
|
13
|
+
monitor.should_not be_closed
|
14
|
+
end
|
15
|
+
|
16
|
+
it "knows which IO objects are registered" do
|
17
|
+
reader, writer = IO.pipe
|
18
|
+
subject.register(reader, :r)
|
19
|
+
|
20
|
+
subject.should be_registered(reader)
|
21
|
+
subject.should_not be_registered(writer)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "deregisters IO objects" do
|
25
|
+
pipe, _ = IO.pipe
|
26
|
+
|
27
|
+
subject.register(pipe, :r)
|
28
|
+
monitor = subject.deregister(pipe)
|
29
|
+
subject.should_not be_registered(pipe)
|
30
|
+
monitor.should be_closed
|
31
|
+
end
|
32
|
+
|
33
|
+
context "select" do
|
34
|
+
it "waits for a timeout when selecting" do
|
35
|
+
reader, writer = IO.pipe
|
36
|
+
monitor = subject.register(reader, :r)
|
37
|
+
|
38
|
+
payload = "hi there"
|
39
|
+
writer << payload
|
40
|
+
|
41
|
+
timeout = 0.5
|
42
|
+
started_at = Time.now
|
43
|
+
subject.select(timeout).should include monitor
|
44
|
+
(Time.now - started_at).should be_within(TIMEOUT_PRECISION).of(0)
|
45
|
+
reader.read_nonblock(payload.size)
|
46
|
+
|
47
|
+
started_at = Time.now
|
48
|
+
subject.select(timeout).should be_nil
|
49
|
+
(Time.now - started_at).should be_within(TIMEOUT_PRECISION).of(timeout)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "wakes up if signaled to from another thread" do
|
53
|
+
pipe, _ = IO.pipe
|
54
|
+
subject.register(pipe, :r)
|
55
|
+
|
56
|
+
thread = Thread.new do
|
57
|
+
started_at = Time.now
|
58
|
+
subject.select.should be_nil
|
59
|
+
Time.now - started_at
|
60
|
+
end
|
61
|
+
|
62
|
+
timeout = 0.1
|
63
|
+
sleep timeout
|
64
|
+
subject.wakeup
|
65
|
+
|
66
|
+
thread.value.should be_within(TIMEOUT_PRECISION).of(timeout)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it "closes" do
|
71
|
+
subject.close
|
72
|
+
subject.should be_closed
|
73
|
+
end
|
74
|
+
|
75
|
+
context "selectables" do
|
76
|
+
shared_context "an NIO selectable" do
|
77
|
+
it "selects for read readiness" do
|
78
|
+
waiting_monitor = subject.register(unreadable_subject, :r)
|
79
|
+
ready_monitor = subject.register(readable_subject, :r)
|
80
|
+
|
81
|
+
ready_monitors = subject.select
|
82
|
+
ready_monitors.should include ready_monitor
|
83
|
+
ready_monitors.should_not include waiting_monitor
|
84
|
+
end
|
85
|
+
|
86
|
+
it "selects for write readiness" do
|
87
|
+
waiting_monitor = subject.register(unwritable_subject, :w)
|
88
|
+
ready_monitor = subject.register(writable_subject, :w)
|
89
|
+
|
90
|
+
ready_monitors = subject.select(0.1)
|
91
|
+
|
92
|
+
ready_monitors.should include ready_monitor
|
93
|
+
ready_monitors.should_not include waiting_monitor
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "IO.pipe" do
|
98
|
+
let :readable_subject do
|
99
|
+
pipe, peer = IO.pipe
|
100
|
+
peer << "data"
|
101
|
+
pipe
|
102
|
+
end
|
103
|
+
|
104
|
+
let :unreadable_subject do
|
105
|
+
pipe, _ = IO.pipe
|
106
|
+
pipe
|
107
|
+
end
|
108
|
+
|
109
|
+
let :writable_subject do
|
110
|
+
_, pipe = IO.pipe
|
111
|
+
pipe
|
112
|
+
end
|
113
|
+
|
114
|
+
let :unwritable_subject do
|
115
|
+
reader, pipe = IO.pipe
|
116
|
+
|
117
|
+
begin
|
118
|
+
pipe.write_nonblock "JUNK IN THE TUBES"
|
119
|
+
_, writers = select [], [pipe], [], 0
|
120
|
+
rescue Errno::EPIPE
|
121
|
+
break
|
122
|
+
end while writers and writers.include? pipe
|
123
|
+
|
124
|
+
pipe
|
125
|
+
end
|
126
|
+
|
127
|
+
it_behaves_like "an NIO selectable"
|
128
|
+
end
|
129
|
+
|
130
|
+
context TCPSocket do
|
131
|
+
let(:tcp_port) { 12345 }
|
132
|
+
|
133
|
+
let :readable_subject do
|
134
|
+
server = TCPServer.new("localhost", tcp_port)
|
135
|
+
sock = TCPSocket.open("localhost", tcp_port)
|
136
|
+
peer = server.accept
|
137
|
+
peer << "data"
|
138
|
+
sock
|
139
|
+
end
|
140
|
+
|
141
|
+
let :unreadable_subject do
|
142
|
+
TCPServer.new("localhost", tcp_port + 1)
|
143
|
+
TCPSocket.open("localhost", tcp_port + 1)
|
144
|
+
end
|
145
|
+
|
146
|
+
let :writable_subject do
|
147
|
+
TCPServer.new("localhost", tcp_port + 2)
|
148
|
+
TCPSocket.open("localhost", tcp_port + 2)
|
149
|
+
end
|
150
|
+
|
151
|
+
let :unwritable_subject do
|
152
|
+
server = TCPServer.new("localhost", tcp_port + 3)
|
153
|
+
sock = TCPSocket.open("localhost", tcp_port + 3)
|
154
|
+
peer = server.accept
|
155
|
+
|
156
|
+
begin
|
157
|
+
sock.write_nonblock "JUNK IN THE TUBES"
|
158
|
+
_, writers = select [], [sock], [], 0
|
159
|
+
end while writers and writers.include? sock
|
160
|
+
|
161
|
+
sock
|
162
|
+
end
|
163
|
+
|
164
|
+
it_behaves_like "an NIO selectable"
|
165
|
+
end
|
166
|
+
|
167
|
+
context UDPSocket do
|
168
|
+
let(:udp_port) { 23456 }
|
169
|
+
|
170
|
+
let :readable_subject do
|
171
|
+
sock = UDPSocket.new
|
172
|
+
sock.bind('localhost', udp_port)
|
173
|
+
|
174
|
+
peer = UDPSocket.new
|
175
|
+
peer.send("hi there", 0, 'localhost', udp_port)
|
176
|
+
|
177
|
+
sock
|
178
|
+
end
|
179
|
+
|
180
|
+
let :unreadable_subject do
|
181
|
+
sock = UDPSocket.new
|
182
|
+
sock.bind('localhost', udp_port + 1)
|
183
|
+
sock
|
184
|
+
end
|
185
|
+
|
186
|
+
let :writable_subject do
|
187
|
+
pending "come up with a writable UDPSocket example"
|
188
|
+
end
|
189
|
+
|
190
|
+
let :unwritable_subject do
|
191
|
+
pending "come up with a UDPSocket that's blocked on writing"
|
192
|
+
end
|
193
|
+
|
194
|
+
it_behaves_like "an NIO selectable"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|