ione 1.0.0.pre0

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