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/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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module NIO
2
+ VERSION = "0.0.1"
3
+ end
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