ione 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/ione/byte_buffer.rb +2 -0
- data/lib/ione/future.rb +91 -27
- data/lib/ione/io/acceptor.rb +27 -6
- data/lib/ione/io/base_connection.rb +25 -13
- data/lib/ione/io/connection.rb +3 -2
- data/lib/ione/io/io_reactor.rb +171 -50
- data/lib/ione/io/server_connection.rb +2 -1
- data/lib/ione/io/ssl_connection.rb +1 -1
- data/lib/ione/io/ssl_server_connection.rb +7 -4
- data/lib/ione/version.rb +1 -1
- data/spec/ione/future_spec.rb +112 -0
- data/spec/ione/io/connection_spec.rb +4 -4
- data/spec/ione/io/io_reactor_spec.rb +235 -18
- metadata +2 -2
data/lib/ione/io/connection.rb
CHANGED
@@ -4,6 +4,7 @@ module Ione
|
|
4
4
|
module Io
|
5
5
|
# A wrapper around a socket. Handles connecting to the remote host, reading
|
6
6
|
# from and writing to the socket.
|
7
|
+
# @since v1.0.0
|
7
8
|
class Connection < BaseConnection
|
8
9
|
attr_reader :connection_timeout
|
9
10
|
|
@@ -31,11 +32,11 @@ module Ione
|
|
31
32
|
end
|
32
33
|
unless connected?
|
33
34
|
@io.connect_nonblock(@sockaddr)
|
34
|
-
@state =
|
35
|
+
@state = CONNECTED_STATE
|
35
36
|
@connected_promise.fulfill(self)
|
36
37
|
end
|
37
38
|
rescue Errno::EISCONN
|
38
|
-
@state =
|
39
|
+
@state = CONNECTED_STATE
|
39
40
|
@connected_promise.fulfill(self)
|
40
41
|
rescue Errno::EINPROGRESS, Errno::EALREADY
|
41
42
|
if @clock.now - @connection_started_at > @connection_timeout
|
data/lib/ione/io/io_reactor.rb
CHANGED
@@ -78,20 +78,25 @@ module Ione
|
|
78
78
|
# # get data back.
|
79
79
|
# end
|
80
80
|
# end
|
81
|
+
#
|
82
|
+
# @since v1.0.0
|
81
83
|
class IoReactor
|
84
|
+
PENDING_STATE = 0
|
85
|
+
RUNNING_STATE = 1
|
86
|
+
CRASHED_STATE = 2
|
87
|
+
STOPPING_STATE = 3
|
88
|
+
STOPPED_STATE = 4
|
89
|
+
|
82
90
|
# Initializes a new IO reactor.
|
83
91
|
#
|
84
92
|
# @param options [Hash] only used to inject behaviour during tests
|
85
93
|
def initialize(options={})
|
94
|
+
@options = options
|
86
95
|
@clock = options[:clock] || Time
|
87
|
-
@
|
88
|
-
@
|
89
|
-
@io_loop.
|
96
|
+
@state = PENDING_STATE
|
97
|
+
@error_listeners = []
|
98
|
+
@io_loop = IoLoopBody.new(@options)
|
90
99
|
@scheduler = Scheduler.new
|
91
|
-
@running = false
|
92
|
-
@stopped = false
|
93
|
-
@started_promise = Promise.new
|
94
|
-
@stopped_promise = Promise.new
|
95
100
|
@lock = Mutex.new
|
96
101
|
end
|
97
102
|
|
@@ -103,14 +108,23 @@ module Ione
|
|
103
108
|
#
|
104
109
|
# @yield [error] the error that cause the reactor to stop
|
105
110
|
def on_error(&listener)
|
106
|
-
@
|
111
|
+
@lock.lock
|
112
|
+
begin
|
113
|
+
@error_listeners = @error_listeners.dup
|
114
|
+
@error_listeners << listener
|
115
|
+
ensure
|
116
|
+
@lock.unlock
|
117
|
+
end
|
118
|
+
if @state == RUNNING_STATE || @state == CRASHED_STATE
|
119
|
+
@stopped_promise.future.on_failure(&listener)
|
120
|
+
end
|
107
121
|
end
|
108
122
|
|
109
123
|
# Returns true as long as the reactor is running. It will be true even
|
110
124
|
# after {#stop} has been called, but false when the future returned by
|
111
125
|
# {#stop} completes.
|
112
126
|
def running?
|
113
|
-
@
|
127
|
+
@state == RUNNING_STATE
|
114
128
|
end
|
115
129
|
|
116
130
|
# Starts the reactor. This will spawn a background thread that will manage
|
@@ -122,25 +136,41 @@ module Ione
|
|
122
136
|
# @return [Ione::Future] a future that will resolve to the reactor itself
|
123
137
|
def start
|
124
138
|
@lock.synchronize do
|
125
|
-
|
126
|
-
|
127
|
-
@
|
139
|
+
if @state == RUNNING_STATE
|
140
|
+
return @started_promise.future
|
141
|
+
elsif @state == STOPPING_STATE
|
142
|
+
return @stopped_promise.future.flat_map { start }.fallback { start }
|
143
|
+
else
|
144
|
+
@state = RUNNING_STATE
|
145
|
+
end
|
146
|
+
end
|
147
|
+
@unblocker = Unblocker.new
|
148
|
+
@io_loop.add_socket(@unblocker)
|
149
|
+
@started_promise = Promise.new
|
150
|
+
@stopped_promise = Promise.new
|
151
|
+
@error_listeners.each do |listener|
|
152
|
+
@stopped_promise.future.on_failure(&listener)
|
128
153
|
end
|
129
154
|
Thread.start do
|
130
155
|
@started_promise.fulfill(self)
|
131
156
|
begin
|
132
|
-
|
157
|
+
while @state == RUNNING_STATE
|
133
158
|
@io_loop.tick
|
134
159
|
@scheduler.tick
|
135
160
|
end
|
136
161
|
ensure
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
162
|
+
begin
|
163
|
+
@io_loop.close_sockets
|
164
|
+
@scheduler.cancel_timers
|
165
|
+
@unblocker = nil
|
166
|
+
ensure
|
167
|
+
if $!
|
168
|
+
@state = CRASHED_STATE
|
169
|
+
@stopped_promise.fail($!)
|
170
|
+
else
|
171
|
+
@state = STOPPED_STATE
|
172
|
+
@stopped_promise.fulfill(self)
|
173
|
+
end
|
144
174
|
end
|
145
175
|
end
|
146
176
|
end
|
@@ -155,12 +185,29 @@ module Ione
|
|
155
185
|
#
|
156
186
|
# @return [Ione::Future] a future that will resolve to the reactor itself
|
157
187
|
def stop
|
158
|
-
@
|
159
|
-
|
188
|
+
@lock.synchronize do
|
189
|
+
if @state == PENDING_STATE
|
190
|
+
Future.resolved(self)
|
191
|
+
elsif @state != STOPPED_STATE && @state != CRASHED_STATE
|
192
|
+
@state = STOPPING_STATE
|
193
|
+
@stopped_promise.future
|
194
|
+
else
|
195
|
+
@stopped_promise.future
|
196
|
+
end
|
197
|
+
end
|
160
198
|
end
|
161
199
|
|
162
200
|
# Opens a connection to the specified host and port.
|
163
201
|
#
|
202
|
+
# @example A naive HTTP client
|
203
|
+
# connection_future = reactor.connect('example.com', 80)
|
204
|
+
# connection_future.on_value do |connection|
|
205
|
+
# connection.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
206
|
+
# connection.on_data do |data|
|
207
|
+
# print(data)
|
208
|
+
# end
|
209
|
+
# end
|
210
|
+
#
|
164
211
|
# @param host [String] the host to connect to
|
165
212
|
# @param port [Integer] the port to connect to
|
166
213
|
# @param options [Hash, Numeric] a hash of options (see below)
|
@@ -172,8 +219,8 @@ module Ione
|
|
172
219
|
# or true to upgrade the connection and create a new context.
|
173
220
|
# @yieldparam [Ione::Io::Connection] connection the newly opened connection
|
174
221
|
# @return [Ione::Future] a future that will resolve when the connection is
|
175
|
-
# open. The value will be the connection, or when a block is given
|
176
|
-
#
|
222
|
+
# open. The value will be the connection, or when a block is given the
|
223
|
+
# value returned by the block.
|
177
224
|
def connect(host, port, options=nil, &block)
|
178
225
|
if options.is_a?(Numeric) || options.nil?
|
179
226
|
timeout = options || 5
|
@@ -185,7 +232,7 @@ module Ione
|
|
185
232
|
connection = Connection.new(host, port, timeout, @unblocker, @clock)
|
186
233
|
f = connection.connect
|
187
234
|
@io_loop.add_socket(connection)
|
188
|
-
@unblocker.unblock
|
235
|
+
@unblocker.unblock if running?
|
189
236
|
if ssl
|
190
237
|
f = f.flat_map do
|
191
238
|
ssl_context = ssl == true ? nil : ssl
|
@@ -201,6 +248,60 @@ module Ione
|
|
201
248
|
f
|
202
249
|
end
|
203
250
|
|
251
|
+
# Starts a server bound to the specified host and port.
|
252
|
+
#
|
253
|
+
# A server is represented by an {Acceptor}, which wraps the server socket
|
254
|
+
# and accepts client connections. By registering to be notified on new
|
255
|
+
# connections, via {Acceptor#on_accept}, you can attach your server
|
256
|
+
# handling code to a connection.
|
257
|
+
#
|
258
|
+
# @example An echo server
|
259
|
+
# acceptor_future = reactor.bind('0.0.0.0', 11111)
|
260
|
+
# acceptor_future.on_value do |acceptor|
|
261
|
+
# acceptor.on_accept do |connection|
|
262
|
+
# connection.on_data do |data|
|
263
|
+
# connection.write(data)
|
264
|
+
# end
|
265
|
+
# end
|
266
|
+
# end
|
267
|
+
#
|
268
|
+
# @example A more realistic server template
|
269
|
+
# class EchoServer
|
270
|
+
# def initialize(acceptor)
|
271
|
+
# @acceptor = acceptor
|
272
|
+
# @acceptor.on_accept do |connection|
|
273
|
+
# handle_connection(connection)
|
274
|
+
# end
|
275
|
+
# end
|
276
|
+
#
|
277
|
+
# def handle_connection(connection)
|
278
|
+
# connection.on_data do |data|
|
279
|
+
# connection.write(data)
|
280
|
+
# end
|
281
|
+
# end
|
282
|
+
# end
|
283
|
+
#
|
284
|
+
# server_future = reactor.bind('0.0.0.0', 11111) do |acceptor|
|
285
|
+
# EchoServer.new(acceptor)
|
286
|
+
# end
|
287
|
+
#
|
288
|
+
# server_future.on_value do |echo_server|
|
289
|
+
# # this is called when the server has started
|
290
|
+
# end
|
291
|
+
#
|
292
|
+
# @param host [String] the host to bind to, for example 127.0.0.1,
|
293
|
+
# 'example.com' – or '0.0.0.0' to bind to all interfaces
|
294
|
+
# @param port [Integer] the port to bind to
|
295
|
+
# @param options [Hash]
|
296
|
+
# @option options [Integer] :backlog (5) the maximum number of pending
|
297
|
+
# (unaccepted) connections, i.e. Socket::SOMAXCONN
|
298
|
+
# @option options [OpenSSL::SSL::SSLContext] :ssl (nil) when specified the
|
299
|
+
# server will use this SSLContext to encrypt connections
|
300
|
+
# @yieldparam [Ione::Io::Acceptor] the acceptor instance for this server
|
301
|
+
# @return [Ione::Future] a future that will resolve when the server is
|
302
|
+
# bound. The value will be the acceptor, or when a block is given, the
|
303
|
+
# value returned by the block.
|
304
|
+
# @since v1.1.0
|
204
305
|
def bind(host, port, options=nil, &block)
|
205
306
|
if options.is_a?(Integer) || options.nil?
|
206
307
|
backlog = options || 5
|
@@ -216,7 +317,7 @@ module Ione
|
|
216
317
|
end
|
217
318
|
f = server.bind
|
218
319
|
@io_loop.add_socket(server)
|
219
|
-
@unblocker.unblock
|
320
|
+
@unblocker.unblock if running?
|
220
321
|
f = f.map(&block) if block_given?
|
221
322
|
f
|
222
323
|
end
|
@@ -241,21 +342,31 @@ module Ione
|
|
241
342
|
# The timer will fail with a {Ione::CancelledError}.
|
242
343
|
#
|
243
344
|
# @param timer_future [Ione::Future] the future returned by {#schedule_timer}
|
345
|
+
# @since v1.1.3
|
244
346
|
def cancel_timer(timer_future)
|
245
347
|
@scheduler.cancel_timer(timer_future)
|
246
348
|
end
|
247
349
|
|
248
350
|
def to_s
|
249
|
-
|
351
|
+
state_constant_name = self.class.constants.find do |name|
|
352
|
+
name.to_s.end_with?('_STATE') && self.class.const_get(name) == @state
|
353
|
+
end
|
354
|
+
state = state_constant_name.to_s.rpartition('_').first
|
355
|
+
%(#<#{self.class.name} #{state}>)
|
250
356
|
end
|
251
357
|
end
|
252
358
|
|
253
359
|
# @private
|
254
360
|
class Unblocker
|
361
|
+
OPEN_STATE = 0
|
362
|
+
CLOSED_STATE = 1
|
363
|
+
|
255
364
|
def initialize
|
256
365
|
@out, @in = IO.pipe
|
257
366
|
@lock = Mutex.new
|
258
|
-
@state =
|
367
|
+
@state = OPEN_STATE
|
368
|
+
@unblocked = false
|
369
|
+
@writables = [@in]
|
259
370
|
end
|
260
371
|
|
261
372
|
def connected?
|
@@ -271,14 +382,16 @@ module Ione
|
|
271
382
|
end
|
272
383
|
|
273
384
|
def closed?
|
274
|
-
@state ==
|
385
|
+
@state == CLOSED_STATE
|
275
386
|
end
|
276
387
|
|
277
388
|
def unblock
|
278
|
-
|
389
|
+
if @state != CLOSED_STATE
|
279
390
|
@lock.lock
|
280
391
|
begin
|
281
|
-
@
|
392
|
+
if @state != CLOSED_STATE && IO.select(nil, @writables, nil, 0)
|
393
|
+
@in.write_nonblock(PING_BYTE)
|
394
|
+
end
|
282
395
|
ensure
|
283
396
|
@lock.unlock
|
284
397
|
end
|
@@ -286,15 +399,19 @@ module Ione
|
|
286
399
|
end
|
287
400
|
|
288
401
|
def read
|
289
|
-
|
290
|
-
|
402
|
+
@lock.lock
|
403
|
+
if @state != CLOSED_STATE
|
404
|
+
@out.read_nonblock(65536)
|
405
|
+
@unblocked = false
|
291
406
|
end
|
407
|
+
ensure
|
408
|
+
@lock.unlock
|
292
409
|
end
|
293
410
|
|
294
411
|
def close
|
295
412
|
@lock.synchronize do
|
296
|
-
return if @state ==
|
297
|
-
@state =
|
413
|
+
return if @state == CLOSED_STATE
|
414
|
+
@state = CLOSED_STATE
|
298
415
|
end
|
299
416
|
@in.close
|
300
417
|
@out.close
|
@@ -374,6 +491,7 @@ module Ione
|
|
374
491
|
# the socket had most likely already closed due to an error
|
375
492
|
end
|
376
493
|
end
|
494
|
+
@sockets = []
|
377
495
|
end
|
378
496
|
|
379
497
|
def tick
|
@@ -452,25 +570,28 @@ module Ione
|
|
452
570
|
timers.each do |timer|
|
453
571
|
timer.fail(CancelledError.new)
|
454
572
|
end
|
573
|
+
nil
|
455
574
|
end
|
456
575
|
|
457
576
|
def tick
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
@timer_queue.
|
466
|
-
|
467
|
-
|
577
|
+
unless @timer_queue.empty?
|
578
|
+
now = @clock.now
|
579
|
+
first_timer = @timer_queue.peek
|
580
|
+
if first_timer && first_timer.time <= now
|
581
|
+
expired_timers = []
|
582
|
+
@lock.lock
|
583
|
+
begin
|
584
|
+
while (timer = @timer_queue.peek) && timer.time <= now
|
585
|
+
@timer_queue.pop
|
586
|
+
@pending_timers.delete(timer.future)
|
587
|
+
expired_timers << timer
|
588
|
+
end
|
589
|
+
ensure
|
590
|
+
@lock.unlock
|
591
|
+
end
|
592
|
+
expired_timers.each do |timer|
|
593
|
+
timer.fulfill
|
468
594
|
end
|
469
|
-
ensure
|
470
|
-
@lock.unlock
|
471
|
-
end
|
472
|
-
expired_timers.each do |timer|
|
473
|
-
timer.fulfill
|
474
595
|
end
|
475
596
|
end
|
476
597
|
end
|
@@ -2,12 +2,13 @@
|
|
2
2
|
|
3
3
|
module Ione
|
4
4
|
module Io
|
5
|
+
# @since v1.1.0
|
5
6
|
class ServerConnection < BaseConnection
|
6
7
|
# @private
|
7
8
|
def initialize(socket, host, port, unblocker)
|
8
9
|
super(host, port, unblocker)
|
9
10
|
@io = socket
|
10
|
-
@state =
|
11
|
+
@state = CONNECTED_STATE
|
11
12
|
end
|
12
13
|
end
|
13
14
|
end
|
@@ -4,17 +4,20 @@ module Ione
|
|
4
4
|
module Io
|
5
5
|
# @private
|
6
6
|
class SslServerConnection < ServerConnection
|
7
|
+
ACCEPTING_STATE = 0
|
8
|
+
ESTABLISHED_STATE = 1
|
9
|
+
|
7
10
|
def initialize(socket, host, port, unblocker, ssl_context, accept_callback, ssl_socket_impl=nil)
|
8
11
|
super(socket, host, port, unblocker)
|
9
12
|
@ssl_context = ssl_context
|
10
13
|
@accept_callback = accept_callback
|
11
14
|
@ssl_socket_impl = ssl_socket_impl || OpenSSL::SSL::SSLSocket
|
12
|
-
@ssl_state =
|
15
|
+
@ssl_state = ACCEPTING_STATE
|
13
16
|
end
|
14
17
|
|
15
18
|
# @private
|
16
19
|
def to_io
|
17
|
-
if @ssl_state ==
|
20
|
+
if @ssl_state == ESTABLISHED_STATE
|
18
21
|
@io.to_io
|
19
22
|
else
|
20
23
|
@io
|
@@ -22,12 +25,12 @@ module Ione
|
|
22
25
|
end
|
23
26
|
|
24
27
|
def read
|
25
|
-
if @ssl_state ==
|
28
|
+
if @ssl_state == ACCEPTING_STATE
|
26
29
|
begin
|
27
30
|
@ssl_io ||= @ssl_socket_impl.new(@io, @ssl_context)
|
28
31
|
@ssl_io.accept_nonblock
|
29
32
|
@io = @ssl_io
|
30
|
-
@ssl_state =
|
33
|
+
@ssl_state = ESTABLISHED_STATE
|
31
34
|
@accept_callback.call(self)
|
32
35
|
rescue IO::WaitReadable
|
33
36
|
# connection not ready yet
|
data/lib/ione/version.rb
CHANGED