ione 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = :connected
35
+ @state = CONNECTED_STATE
35
36
  @connected_promise.fulfill(self)
36
37
  end
37
38
  rescue Errno::EISCONN
38
- @state = :connected
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
@@ -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
- @unblocker = Unblocker.new
88
- @io_loop = IoLoopBody.new(options)
89
- @io_loop.add_socket(@unblocker)
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
- @stopped_promise.future.on_failure(&listener)
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
- @running
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
- raise ReactorError, 'Cannot start a stopped IO reactor' if @stopped
126
- return @started_promise.future if @running
127
- @running = true
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
- until @stopped
157
+ while @state == RUNNING_STATE
133
158
  @io_loop.tick
134
159
  @scheduler.tick
135
160
  end
136
161
  ensure
137
- @io_loop.close_sockets
138
- @scheduler.cancel_timers
139
- @running = false
140
- if $!
141
- @stopped_promise.fail($!)
142
- else
143
- @stopped_promise.fulfill(self)
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
- @stopped = true
159
- @stopped_promise.future
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 to
176
- # what the block returns
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
- @io_loop.to_s
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 = :open
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 == :closed
385
+ @state == CLOSED_STATE
275
386
  end
276
387
 
277
388
  def unblock
278
- unless closed?
389
+ if @state != CLOSED_STATE
279
390
  @lock.lock
280
391
  begin
281
- @in.write(PING_BYTE)
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
- unless closed?
290
- @out.read_nonblock(2**16)
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 == :closed
297
- @state = :closed
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
- now = @clock.now
459
- first_timer = @timer_queue.peek
460
- if first_timer && first_timer.time <= now
461
- expired_timers = []
462
- @lock.lock
463
- begin
464
- while (timer = @timer_queue.peek) && timer.time <= now
465
- @timer_queue.pop
466
- @pending_timers.delete(timer.future)
467
- expired_timers << timer
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 = :connected
11
+ @state = CONNECTED_STATE
11
12
  end
12
13
  end
13
14
  end
@@ -23,7 +23,7 @@ module Ione
23
23
  @io = @socket_impl.new(@raw_io)
24
24
  end
25
25
  @io.connect_nonblock
26
- @state = :connected
26
+ @state = CONNECTED_STATE
27
27
  @connected_promise.fulfill(self)
28
28
  @connected_promise.future
29
29
  rescue IO::WaitReadable, IO::WaitWritable
@@ -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 = :accepting
15
+ @ssl_state = ACCEPTING_STATE
13
16
  end
14
17
 
15
18
  # @private
16
19
  def to_io
17
- if @ssl_state == :established
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 == :accepting
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 = :established
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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Ione
4
- VERSION = '1.2.0'.freeze
4
+ VERSION = '1.2.1'.freeze
5
5
  end