ione 1.0.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ione/io.rb ADDED
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ module Ione
4
+ CancelledError = Class.new(StandardError)
5
+ IoError = Class.new(StandardError)
6
+
7
+ module Io
8
+ ConnectionError = Class.new(IoError)
9
+ ConnectionClosedError = Class.new(ConnectionError)
10
+ ConnectionTimeoutError = Class.new(ConnectionError)
11
+ end
12
+ end
13
+
14
+ require 'ione/io/io_reactor'
15
+ require 'ione/io/connection'
@@ -0,0 +1,215 @@
1
+ # encoding: utf-8
2
+
3
+ require 'socket'
4
+
5
+
6
+ module Ione
7
+ module Io
8
+ # A wrapper around a socket. Handles connecting to the remote host, reading
9
+ # from and writing to the socket.
10
+ class Connection
11
+ attr_reader :host, :port, :connection_timeout
12
+
13
+ # @private
14
+ def initialize(host, port, connection_timeout, unblocker, clock, socket_impl=Socket)
15
+ @host = host
16
+ @port = port
17
+ @connection_timeout = connection_timeout
18
+ @unblocker = unblocker
19
+ @clock = clock
20
+ @socket_impl = socket_impl
21
+ @lock = Mutex.new
22
+ @connected = false
23
+ @write_buffer = ByteBuffer.new
24
+ @connected_promise = Promise.new
25
+ end
26
+
27
+ # @private
28
+ def connect
29
+ begin
30
+ unless @addrinfos
31
+ @connection_started_at = @clock.now
32
+ @addrinfos = @socket_impl.getaddrinfo(@host, @port, nil, Socket::SOCK_STREAM)
33
+ end
34
+ unless @io
35
+ _, port, _, ip, address_family, socket_type = @addrinfos.shift
36
+ @sockaddr = @socket_impl.sockaddr_in(port, ip)
37
+ @io = @socket_impl.new(address_family, socket_type, 0)
38
+ end
39
+ unless connected?
40
+ @io.connect_nonblock(@sockaddr)
41
+ @connected = true
42
+ @connected_promise.fulfill(self)
43
+ end
44
+ rescue Errno::EISCONN
45
+ @connected = true
46
+ @connected_promise.fulfill(self)
47
+ rescue Errno::EINPROGRESS, Errno::EALREADY
48
+ if @clock.now - @connection_started_at > @connection_timeout
49
+ close(ConnectionTimeoutError.new("Could not connect to #{@host}:#{@port} within #{@connection_timeout}s"))
50
+ end
51
+ rescue Errno::EINVAL, Errno::ECONNREFUSED => e
52
+ if @addrinfos.empty?
53
+ close(e)
54
+ else
55
+ @io = nil
56
+ retry
57
+ end
58
+ rescue SystemCallError => e
59
+ close(e)
60
+ rescue SocketError => e
61
+ close(e) || closed!(e)
62
+ end
63
+ @connected_promise.future
64
+ end
65
+
66
+ # Closes the connection
67
+ def close(cause=nil)
68
+ return false unless @io
69
+ begin
70
+ @io.close
71
+ rescue SystemCallError, IOError
72
+ # nothing to do, the socket was most likely already closed
73
+ end
74
+ closed!(cause)
75
+ true
76
+ end
77
+
78
+ # @private
79
+ def connecting?
80
+ !(closed? || connected?)
81
+ end
82
+
83
+ # Returns true if the connection is connected
84
+ def connected?
85
+ @connected
86
+ end
87
+
88
+ # Returns true if the connection is closed
89
+ def closed?
90
+ @io.nil?
91
+ end
92
+
93
+ # @private
94
+ def writable?
95
+ empty_buffer = @lock.synchronize do
96
+ @write_buffer.empty?
97
+ end
98
+ !(closed? || empty_buffer)
99
+ end
100
+
101
+ # Register to receive notifications when new data is read from the socket.
102
+ #
103
+ # You should only call this method in your protocol handler constructor.
104
+ #
105
+ # Only one callback can be registered, if you register multiple times only
106
+ # the last one will receive notifications. This is not meant as a general
107
+ # event system, it's just for protocol handlers to receive data from their
108
+ # connection. If you want multiple listeners you need to implement that
109
+ # yourself in your protocol handler.
110
+ #
111
+ # It is very important that you don't do any heavy lifting in the callback
112
+ # since it is called from the IO reactor thread, and as long as the
113
+ # callback is working the reactor can't handle any IO and no other
114
+ # callbacks can be called.
115
+ #
116
+ # Errors raised by the callback will be ignored.
117
+ #
118
+ # @yield [String] the new data
119
+ def on_data(&listener)
120
+ @data_listener = listener
121
+ end
122
+
123
+ # Register to receive a notification when the socket is closed, both for
124
+ # expected and unexpected reasons.
125
+ #
126
+ # You shoud only call this method in your protocol handler constructor.
127
+ #
128
+ # Only one callback can be registered, if you register multiple times only
129
+ # the last one will receive notifications. This is not meant as a general
130
+ # event system, it's just for protocol handlers to be notified of the
131
+ # connection closing. If you want multiple listeners you need to implement
132
+ # that yourself in your protocol handler.
133
+ #
134
+ # Errors raised by the callback will be ignored.
135
+ #
136
+ # @yield [error, nil] the error that caused the socket to close, or nil if
137
+ # the socket closed with #close
138
+ def on_closed(&listener)
139
+ @closed_listener = listener
140
+ end
141
+
142
+ # Write bytes to the socket.
143
+ #
144
+ # You can either pass in bytes (as a string or as a `ByteBuffer`), or you
145
+ # can use the block form of this method to get access to the connection's
146
+ # internal buffer.
147
+ #
148
+ # @yieldparam buffer [Ione::ByteBuffer] the connection's internal buffer
149
+ # @param bytes [String, Ione::ByteBuffer] the data to write to the socket
150
+ def write(bytes=nil)
151
+ @lock.synchronize do
152
+ if block_given?
153
+ yield @write_buffer
154
+ elsif bytes
155
+ @write_buffer.append(bytes)
156
+ end
157
+ end
158
+ @unblocker.unblock!
159
+ end
160
+
161
+ # @private
162
+ def flush
163
+ if writable?
164
+ @lock.synchronize do
165
+ bytes_written = @io.write_nonblock(@write_buffer.cheap_peek)
166
+ @write_buffer.discard(bytes_written)
167
+ end
168
+ end
169
+ rescue => e
170
+ close(e)
171
+ end
172
+
173
+ # @private
174
+ def read
175
+ new_data = @io.read_nonblock(2**16)
176
+ @data_listener.call(new_data) if @data_listener
177
+ rescue => e
178
+ close(e)
179
+ end
180
+
181
+ # @private
182
+ def to_io
183
+ @io
184
+ end
185
+
186
+ def to_s
187
+ state = 'inconsistent'
188
+ if connected?
189
+ state = 'connected'
190
+ elsif connecting?
191
+ state = 'connecting'
192
+ elsif closed?
193
+ state = 'closed'
194
+ end
195
+ %(#<#{self.class.name} #{state} #{@host}:#{@port}>)
196
+ end
197
+
198
+ private
199
+
200
+ def closed!(cause)
201
+ @io = nil
202
+ if cause && !cause.is_a?(IoError)
203
+ cause = ConnectionError.new(cause.message)
204
+ end
205
+ unless connected?
206
+ @connected_promise.fail(cause)
207
+ end
208
+ @connected = false
209
+ if @closed_listener
210
+ @closed_listener.call(cause)
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,321 @@
1
+ # encoding: utf-8
2
+
3
+ module Ione
4
+ module Io
5
+ ReactorError = Class.new(IoError)
6
+
7
+ # An IO reactor takes care of all the IO for a client. It handles opening
8
+ # new connections, and making sure that connections that have data to send
9
+ # flush to the network, and connections that have data coming in read that
10
+ # data and delegate it to their protocol handlers.
11
+ #
12
+ # All IO is done in a single background thread, regardless of how many
13
+ # connections you open. There shouldn't be any problems handling hundreds of
14
+ # connections if needed. All operations are thread safe, but you should take
15
+ # great care when in your protocol handlers to make sure that they don't
16
+ # do too much work in their data handling callbacks, since those will be
17
+ # run in the reactor thread, and every cycle you use there is a cycle which
18
+ # can't be used to handle IO.
19
+ #
20
+ # The IO reactor is completely protocol agnostic, and it's up to you to
21
+ # create objects that can interpret the bytes received from remote hosts,
22
+ # and to send the correct commands back. The way this works is that when you
23
+ # open a connection you can provide a protocol handler factory as a block,
24
+ # (or you can simply wrap the returned connection). This factory can be used
25
+ # to create objects that wrap the raw connections and register to receive
26
+ # new data, and it can write data to connection. It can also register to be
27
+ # notified when the socket is closed, or it can itself close the socket.
28
+ #
29
+ # @example A protocol handler that processes whole lines
30
+ # io_reactor.connect('example.com', 6543, 10) do |connection|
31
+ # LineProtocolHandler.new(connection)
32
+ # end
33
+ #
34
+ # # ...
35
+ #
36
+ # class LineProtocolHandler
37
+ # def initialize(connection)
38
+ # @connection = connection
39
+ # # register a listener method for new data, this must be done in the
40
+ # # in the constructor, and only one listener can be registered
41
+ # @connection.on_data(&method(:process_data))
42
+ # @buffer = ''
43
+ # end
44
+ #
45
+ # def process_data(new_data)
46
+ # # in this fictional protocol we want to process whole lines, so we
47
+ # # append new data to our buffer and then loop as long as there is
48
+ # # a newline in the buffer, everything up until a newline is a
49
+ # # complete line
50
+ # @buffer << new_data
51
+ # while newline_index = @buffer.index("\n")
52
+ # line = @buffer.slice!(0, newline_index + 1)
53
+ # line.chomp!
54
+ # # Now do something interesting with the line, but remember that
55
+ # # while you're in the data listener method you're executing in the
56
+ # # IO reactor thread so you're blocking the reactor from doing
57
+ # # other IO work. You should not do any heavy lifting here, but
58
+ # # instead hand off the data to your application's other threads.
59
+ # # One way of doing that is to create a Ione::Future in the method
60
+ # # that sends the request, and then complete the future in this
61
+ # # method. How you keep track of which future belongs to which
62
+ # # reply is very protocol dependent so you'll have to figure that
63
+ # # out yourself.
64
+ # end
65
+ # end
66
+ #
67
+ # def send_request(command_string)
68
+ # # This example primarily shows how to implement a data listener
69
+ # # method, but this is how you write data to the connection. The
70
+ # # method can be called anything, it doesn't have to be #send_request
71
+ # @connection.write(command_string)
72
+ # # The connection object itself is threadsafe, but to create any
73
+ # # interesting protocol you probably need to set up some state for
74
+ # # each request so that you know which request to complete when you
75
+ # # get data back.
76
+ # end
77
+ # end
78
+ class IoReactor
79
+ # Initializes a new IO reactor.
80
+ #
81
+ # @param options [Hash] only used to inject behaviour during tests
82
+ def initialize(options={})
83
+ @clock = options[:clock] || Time
84
+ @unblocker = Unblocker.new
85
+ @io_loop = IoLoopBody.new(options)
86
+ @io_loop.add_socket(@unblocker)
87
+ @running = false
88
+ @stopped = false
89
+ @started_promise = Promise.new
90
+ @stopped_promise = Promise.new
91
+ @lock = Mutex.new
92
+ end
93
+
94
+ # Register to receive notifications when the reactor shuts down because
95
+ # on an irrecoverable error.
96
+ #
97
+ # The listener block will be called in the reactor thread. Any errors that
98
+ # it raises will be ignored.
99
+ #
100
+ # @yield [error] the error that cause the reactor to stop
101
+ def on_error(&listener)
102
+ @stopped_promise.future.on_failure(&listener)
103
+ end
104
+
105
+ # Returns true as long as the reactor is running. It will be true even
106
+ # after {#stop} has been called, but false when the future returned by
107
+ # {#stop} completes.
108
+ def running?
109
+ @running
110
+ end
111
+
112
+ # Starts the reactor. This will spawn a background thread that will manage
113
+ # all connections.
114
+ #
115
+ # This method is asynchronous and returns a future which completes when
116
+ # the reactor has started.
117
+ #
118
+ # @return [Ione::Future] a future that will resolve to the reactor itself
119
+ def start
120
+ @lock.synchronize do
121
+ raise ReactorError, 'Cannot start a stopped IO reactor' if @stopped
122
+ return @started_promise.future if @running
123
+ @running = true
124
+ end
125
+ Thread.start do
126
+ @started_promise.fulfill(self)
127
+ begin
128
+ @io_loop.tick until @stopped
129
+ ensure
130
+ @io_loop.close_sockets
131
+ @io_loop.cancel_timers
132
+ @running = false
133
+ if $!
134
+ @stopped_promise.fail($!)
135
+ else
136
+ @stopped_promise.fulfill(self)
137
+ end
138
+ end
139
+ end
140
+ @started_promise.future
141
+ end
142
+
143
+ # Stops the reactor.
144
+ #
145
+ # This method is asynchronous and returns a future which completes when
146
+ # the reactor has completely stopped, or fails with an error if the reactor
147
+ # stops or has already stopped because of a failure.
148
+ #
149
+ # @return [Ione::Future] a future that will resolve to the reactor itself
150
+ def stop
151
+ @stopped = true
152
+ @stopped_promise.future
153
+ end
154
+
155
+ # Opens a connection to the specified host and port.
156
+ #
157
+ # @param host [String] the host to connect to
158
+ # @param port [Integer] the port to connect to
159
+ # @param timeout [Numeric] the number of seconds to wait for a connection
160
+ # before failing
161
+ # @return [Ione::Future] a future that will resolve to the connection when
162
+ # when it has finished connecting.
163
+ def connect(host, port, timeout)
164
+ connection = Connection.new(host, port, timeout, @unblocker, @clock)
165
+ f = connection.connect
166
+ @io_loop.add_socket(connection)
167
+ @unblocker.unblock!
168
+ f
169
+ end
170
+
171
+ # Returns a future that completes after the specified number of seconds.
172
+ #
173
+ # @param timeout [Float] the number of seconds to wait until the returned
174
+ # future is completed
175
+ # @return [Ione::Future] a future that completes when the timer expires
176
+ def schedule_timer(timeout)
177
+ @io_loop.schedule_timer(timeout)
178
+ end
179
+
180
+ def to_s
181
+ @io_loop.to_s
182
+ end
183
+ end
184
+
185
+ # @private
186
+ class Unblocker
187
+ def initialize
188
+ @out, @in = IO.pipe
189
+ @lock = Mutex.new
190
+ end
191
+
192
+ def connected?
193
+ true
194
+ end
195
+
196
+ def connecting?
197
+ false
198
+ end
199
+
200
+ def writable?
201
+ false
202
+ end
203
+
204
+ def closed?
205
+ @in.nil?
206
+ end
207
+
208
+ def unblock!
209
+ @lock.synchronize do
210
+ @in.write(PING_BYTE)
211
+ end
212
+ end
213
+
214
+ def read
215
+ @out.read_nonblock(2**16)
216
+ end
217
+
218
+ def close
219
+ @in.close
220
+ @out.close
221
+ @in = nil
222
+ @out = nil
223
+ end
224
+
225
+ def to_io
226
+ @out
227
+ end
228
+
229
+ def to_s
230
+ %(#<#{self.class.name}>)
231
+ end
232
+
233
+ private
234
+
235
+ PING_BYTE = "\0".freeze
236
+ end
237
+
238
+ # @private
239
+ class IoLoopBody
240
+ def initialize(options={})
241
+ @selector = options[:selector] || IO
242
+ @clock = options[:clock] || Time
243
+ @lock = Mutex.new
244
+ @sockets = []
245
+ @timers = []
246
+ end
247
+
248
+ def add_socket(socket)
249
+ @lock.synchronize do
250
+ sockets = @sockets.reject { |s| s.closed? }
251
+ sockets << socket
252
+ @sockets = sockets
253
+ end
254
+ end
255
+
256
+ def schedule_timer(timeout, promise=Promise.new)
257
+ @lock.synchronize do
258
+ timers = @timers.reject { |pair| pair[1].nil? }
259
+ timers << [@clock.now + timeout, promise]
260
+ @timers = timers
261
+ end
262
+ promise.future
263
+ end
264
+
265
+ def close_sockets
266
+ @sockets.each do |s|
267
+ begin
268
+ s.close unless s.closed?
269
+ rescue
270
+ # the socket had most likely already closed due to an error
271
+ end
272
+ end
273
+ end
274
+
275
+ def cancel_timers
276
+ @timers.each do |pair|
277
+ if pair[1]
278
+ pair[1].fail(CancelledError.new)
279
+ pair[1] = nil
280
+ end
281
+ end
282
+ end
283
+
284
+ def tick(timeout=1)
285
+ check_sockets!(timeout)
286
+ check_timers!
287
+ end
288
+
289
+ def to_s
290
+ %(#<#{IoReactor.name} @connections=[#{@sockets.map(&:to_s).join(', ')}]>)
291
+ end
292
+
293
+ private
294
+
295
+ def check_sockets!(timeout)
296
+ readables, writables, connecting = [], [], []
297
+ sockets = @sockets
298
+ sockets.each do |s|
299
+ next if s.closed?
300
+ readables << s if s.connected?
301
+ writables << s if s.connecting? || s.writable?
302
+ connecting << s if s.connecting?
303
+ end
304
+ r, w, _ = @selector.select(readables, writables, nil, timeout)
305
+ connecting.each(&:connect)
306
+ r && r.each(&:read)
307
+ w && w.each(&:flush)
308
+ end
309
+
310
+ def check_timers!
311
+ timers = @timers
312
+ timers.each do |pair|
313
+ if pair[1] && pair[0] <= @clock.now
314
+ pair[1].fulfill
315
+ pair[1] = nil
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end