nio4r 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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