ione 1.2.0 → 1.2.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.
- 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