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.
@@ -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