ione 1.0.0 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a27826f7fa9dee9d27d134b297bb4ce709e91c52
4
- data.tar.gz: 151e10014cc157ce72e022491a2935b8cab92aa6
3
+ metadata.gz: 6454a46df7484d84b651eec23a73e5c0e04f3530
4
+ data.tar.gz: e3a48a9e32efc7a42f511b65f6ba248518df01e3
5
5
  SHA512:
6
- metadata.gz: 3e254438d43b4e75ab39c435d49677f277ba6275905752f22ac69c14e90cd843981eeda525cc9385211ea2679b7aa95ae31c8e174a2ad448b5503304d6056fc2
7
- data.tar.gz: 05e82e1103403c4ba0a6af5c99ab712f6f9d4da4d537b1f5327279e324e7d3b5d7baffabc8c59f287fbf30ca41ddc25245711df69b423f78127365f0c61e4e57
6
+ metadata.gz: bad31d689dea8e64182be056b877d7a4b4b12c4bbdebfe5b54c1b6eef1daaac9c9ff7e3705cc61bd68cbc6c1b6eeee7c6e228164de7b08a6cfd6a6bb0862ba7c
7
+ data.tar.gz: 75a8718bbbef223e86776b70e85e3ed4b382702c89f320bdfec5cb594dec95904d9913048283108834d2b7a5640256ee63ade40dfe269e16d6ac9b74ee0a33c4
@@ -0,0 +1,96 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ module Ione
5
+ module Io
6
+ class Acceptor
7
+ ServerSocket = RUBY_ENGINE == 'jruby' ? ::ServerSocket : Socket
8
+
9
+ def initialize(host, port, backlog, unblocker, reactor, socket_impl=nil)
10
+ @host = host
11
+ @port = port
12
+ @backlog = backlog
13
+ @unblocker = unblocker
14
+ @reactor = reactor
15
+ @socket_impl = socket_impl || ServerSocket
16
+ @accept_listeners = []
17
+ @lock = Mutex.new
18
+ end
19
+
20
+ def on_accept(&listener)
21
+ @lock.synchronize do
22
+ @accept_listeners << listener
23
+ end
24
+ end
25
+
26
+ def bind
27
+ addrinfos = @socket_impl.getaddrinfo(@host, @port, nil, Socket::SOCK_STREAM)
28
+ begin
29
+ _, port, _, ip, address_family, socket_type = addrinfos.shift
30
+ @socket = @socket_impl.new(address_family, socket_type, 0)
31
+ bind_socket(@socket, @socket_impl.sockaddr_in(port, ip), @backlog)
32
+ rescue Errno::EADDRNOTAVAIL => e
33
+ if addrinfos.empty?
34
+ raise
35
+ else
36
+ retry
37
+ end
38
+ end
39
+ Future.resolved(self)
40
+ rescue => e
41
+ Future.failed(e)
42
+ end
43
+
44
+ def close
45
+ return false unless @socket
46
+ begin
47
+ @socket.close
48
+ rescue SystemCallError, IOError
49
+ # nothing to do, the socket was most likely already closed
50
+ end
51
+ @socket = nil
52
+ true
53
+ end
54
+
55
+ def to_io
56
+ @socket
57
+ end
58
+
59
+ def closed?
60
+ @socket.nil?
61
+ end
62
+
63
+ def connected?
64
+ !closed?
65
+ end
66
+
67
+ def connecting?
68
+ false
69
+ end
70
+
71
+ def writable?
72
+ false
73
+ end
74
+
75
+ def read
76
+ client_socket, client_sockaddr = @socket.accept_nonblock
77
+ port, host = @socket_impl.unpack_sockaddr_in(client_sockaddr)
78
+ connection = ServerConnection.new(client_socket, host, port, @unblocker)
79
+ @reactor.accept(connection)
80
+ listeners = @lock.synchronize { @accept_listeners }
81
+ listeners.each { |l| l.call(connection) rescue nil }
82
+ end
83
+
84
+ if RUBY_ENGINE == 'jruby'
85
+ def bind_socket(socket, addr, backlog)
86
+ socket.bind(addr, backlog)
87
+ end
88
+ else
89
+ def bind_socket(socket, addr, backlog)
90
+ socket.bind(addr)
91
+ socket.listen(backlog)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+
3
+ module Ione
4
+ module Io
5
+ class BaseConnection
6
+ attr_reader :host, :port
7
+
8
+ def initialize(host, port)
9
+ @host = host
10
+ @port = port
11
+ @state = :connecting
12
+ @closed_promise = Promise.new
13
+ end
14
+
15
+ # Closes the connection
16
+ #
17
+ # @return [true, false] returns false if the connection was already closed
18
+ def close(cause=nil)
19
+ return false if @state == :closed
20
+ if @io
21
+ begin
22
+ @io.close
23
+ @io = nil
24
+ rescue SystemCallError, IOError
25
+ # nothing to do, the socket was most likely already closed
26
+ end
27
+ end
28
+ @state = :closed
29
+ if cause
30
+ @closed_promise.fail(cause)
31
+ else
32
+ @closed_promise.fulfill(self)
33
+ end
34
+ true
35
+ end
36
+
37
+ # Wait for the connection's buffers to empty and then close it.
38
+ #
39
+ # This method is almost always preferable to {#close}.
40
+ #
41
+ # @return [Ione::Future] a future that resolves to the connection when it
42
+ # has closed
43
+ def drain
44
+ @state = :draining
45
+ close if @write_buffer.empty?
46
+ @closed_promise.future
47
+ end
48
+
49
+ # @private
50
+ def connecting?
51
+ @state == :connecting
52
+ end
53
+
54
+ # Returns true if the connection is connected
55
+ def connected?
56
+ @state == :connected
57
+ end
58
+
59
+ # Returns true if the connection is closed
60
+ def closed?
61
+ @state == :closed
62
+ end
63
+
64
+ # @private
65
+ def writable?
66
+ empty_buffer = @lock.synchronize do
67
+ @write_buffer.empty?
68
+ end
69
+ !(closed? || empty_buffer)
70
+ end
71
+
72
+ # Register to receive notifications when new data is read from the socket.
73
+ #
74
+ # You should only call this method in your protocol handler constructor.
75
+ #
76
+ # Only one callback can be registered, if you register multiple times only
77
+ # the last one will receive notifications. This is not meant as a general
78
+ # event system, it's just for protocol handlers to receive data from their
79
+ # connection. If you want multiple listeners you need to implement that
80
+ # yourself in your protocol handler.
81
+ #
82
+ # It is very important that you don't do any heavy lifting in the callback
83
+ # since it is called from the IO reactor thread, and as long as the
84
+ # callback is working the reactor can't handle any IO and no other
85
+ # callbacks can be called.
86
+ #
87
+ # Errors raised by the callback will be ignored.
88
+ #
89
+ # @yield [String] the new data
90
+ def on_data(&listener)
91
+ @data_listener = listener
92
+ end
93
+
94
+ # Register to receive a notification when the socket is closed, both for
95
+ # expected and unexpected reasons.
96
+ #
97
+ # Errors raised by the callback will be ignored.
98
+ #
99
+ # @yield [error, nil] the error that caused the socket to close, or nil if
100
+ # the socket closed with #close
101
+ def on_closed(&listener)
102
+ @closed_promise.future.on_value { listener.call(nil) }
103
+ @closed_promise.future.on_failure { |e| listener.call(e) }
104
+ end
105
+
106
+ # Write bytes to the socket.
107
+ #
108
+ # You can either pass in bytes (as a string or as a `ByteBuffer`), or you
109
+ # can use the block form of this method to get access to the connection's
110
+ # internal buffer.
111
+ #
112
+ # @yieldparam buffer [Ione::ByteBuffer] the connection's internal buffer
113
+ # @param bytes [String, Ione::ByteBuffer] the data to write to the socket
114
+ def write(bytes=nil)
115
+ if @state == :connected || @state == :connecting
116
+ @lock.synchronize do
117
+ if block_given?
118
+ yield @write_buffer
119
+ elsif bytes
120
+ @write_buffer.append(bytes)
121
+ end
122
+ end
123
+ @unblocker.unblock!
124
+ end
125
+ end
126
+
127
+ # @private
128
+ def flush
129
+ if @state == :connected || @state == :draining
130
+ @lock.synchronize do
131
+ unless @write_buffer.empty?
132
+ bytes_written = @io.write_nonblock(@write_buffer.cheap_peek)
133
+ @write_buffer.discard(bytes_written)
134
+ end
135
+ if @state == :draining && @write_buffer.empty?
136
+ close
137
+ end
138
+ end
139
+ end
140
+ rescue => e
141
+ close(e)
142
+ end
143
+
144
+ # @private
145
+ def read
146
+ new_data = @io.read_nonblock(2**16)
147
+ @data_listener.call(new_data) if @data_listener
148
+ rescue => e
149
+ close(e)
150
+ end
151
+
152
+ # @private
153
+ def to_io
154
+ @io
155
+ end
156
+
157
+ def to_s
158
+ %(#<#{self.class.name} #{@state} #{@host}:#{@port}>)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -1,27 +1,23 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'socket'
4
-
5
-
6
3
  module Ione
7
4
  module Io
8
5
  # A wrapper around a socket. Handles connecting to the remote host, reading
9
6
  # from and writing to the socket.
10
- class Connection
11
- attr_reader :host, :port, :connection_timeout
7
+ class Connection < BaseConnection
8
+ attr_reader :connection_timeout
12
9
 
13
10
  # @private
14
11
  def initialize(host, port, connection_timeout, unblocker, clock, socket_impl=Socket)
15
- @host = host
16
- @port = port
12
+ super(host, port)
17
13
  @connection_timeout = connection_timeout
18
14
  @unblocker = unblocker
19
15
  @clock = clock
20
16
  @socket_impl = socket_impl
21
17
  @lock = Mutex.new
22
- @connected = false
23
18
  @write_buffer = ByteBuffer.new
24
19
  @connected_promise = Promise.new
20
+ on_closed(&method(:cleanup_on_close))
25
21
  end
26
22
 
27
23
  # @private
@@ -38,11 +34,11 @@ module Ione
38
34
  end
39
35
  unless connected?
40
36
  @io.connect_nonblock(@sockaddr)
41
- @connected = true
37
+ @state = :connected
42
38
  @connected_promise.fulfill(self)
43
39
  end
44
40
  rescue Errno::EISCONN
45
- @connected = true
41
+ @state = :connected
46
42
  @connected_promise.fulfill(self)
47
43
  rescue Errno::EINPROGRESS, Errno::EALREADY
48
44
  if @clock.now - @connection_started_at > @connection_timeout
@@ -58,157 +54,20 @@ module Ione
58
54
  rescue SystemCallError => e
59
55
  close(e)
60
56
  rescue SocketError => e
61
- close(e) || closed!(e)
57
+ close(e) || cleanup_on_close(e)
62
58
  end
63
59
  @connected_promise.future
64
60
  end
65
61
 
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
62
  private
199
63
 
200
- def closed!(cause)
201
- @io = nil
64
+ def cleanup_on_close(cause)
202
65
  if cause && !cause.is_a?(IoError)
203
66
  cause = ConnectionError.new(cause.message)
204
67
  end
205
- unless connected?
68
+ unless @connected_promise.future.completed?
206
69
  @connected_promise.fail(cause)
207
70
  end
208
- @connected = false
209
- if @closed_listener
210
- @closed_listener.call(cause)
211
- end
212
71
  end
213
72
  end
214
73
  end
@@ -171,6 +171,21 @@ module Ione
171
171
  f
172
172
  end
173
173
 
174
+ def bind(host, port, backlog, &block)
175
+ server = Acceptor.new(host, port, backlog, @unblocker, self)
176
+ f = server.bind
177
+ @io_loop.add_socket(server)
178
+ @unblocker.unblock!
179
+ f = f.map(&block) if block_given?
180
+ f
181
+ end
182
+
183
+ # @private
184
+ def accept(socket)
185
+ @io_loop.add_socket(socket)
186
+ @unblocker.unblock!
187
+ end
188
+
174
189
  # Returns a future that completes after the specified number of seconds.
175
190
  #
176
191
  # @param timeout [Float] the number of seconds to wait until the returned
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Ione
4
+ module Io
5
+ class ServerConnection < BaseConnection
6
+ # @private
7
+ def initialize(socket, host, port, unblocker)
8
+ super(host, port)
9
+ @io = socket
10
+ @unblocker = unblocker
11
+ @lock = Mutex.new
12
+ @write_buffer = ByteBuffer.new
13
+ @state = :connected
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/ione/io.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'socket'
4
+
5
+
3
6
  module Ione
4
7
  CancelledError = Class.new(StandardError)
5
8
  IoError = Class.new(StandardError)
@@ -12,4 +15,7 @@ module Ione
12
15
  end
13
16
 
14
17
  require 'ione/io/io_reactor'
18
+ require 'ione/io/base_connection'
15
19
  require 'ione/io/connection'
20
+ require 'ione/io/server_connection'
21
+ require 'ione/io/acceptor'
data/lib/ione/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Ione
4
- VERSION = '1.0.0'.freeze
4
+ VERSION = '1.1.0.pre0'.freeze
5
5
  end
@@ -8,7 +8,7 @@ describe 'An IO reactor' do
8
8
  Ione::Io::IoReactor.new
9
9
  end
10
10
 
11
- context 'with a generic server' do
11
+ context 'connecting to a generic server' do
12
12
  let :protocol_handler_factory do
13
13
  lambda { |c| IoSpec::TestConnection.new(c) }
14
14
  end
@@ -28,12 +28,12 @@ describe 'An IO reactor' do
28
28
  end
29
29
 
30
30
  it 'connects to the server' do
31
- io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1).map(&protocol_handler_factory)
31
+ io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1, &protocol_handler_factory)
32
32
  fake_server.await_connects!(1)
33
33
  end
34
34
 
35
35
  it 'receives data' do
36
- protocol_handler = io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1).map(&protocol_handler_factory).value
36
+ protocol_handler = io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1, &protocol_handler_factory).value
37
37
  fake_server.await_connects!(1)
38
38
  fake_server.broadcast!('hello world')
39
39
  await { protocol_handler.data.bytesize > 0 }
@@ -41,13 +41,50 @@ describe 'An IO reactor' do
41
41
  end
42
42
 
43
43
  it 'receives data on multiple connections' do
44
- protocol_handlers = Array.new(10) { io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1).map(&protocol_handler_factory).value }
44
+ protocol_handlers = Array.new(10) { io_reactor.connect(ENV['SERVER_HOST'], fake_server.port, 1, &protocol_handler_factory).value }
45
45
  fake_server.await_connects!(10)
46
46
  fake_server.broadcast!('hello world')
47
47
  await { protocol_handlers.all? { |c| c.data.bytesize > 0 } }
48
48
  protocol_handlers.sample.data.should == 'hello world'
49
49
  end
50
50
  end
51
+
52
+ context 'running an echo server' do
53
+ let :protocol_handler_factory do
54
+ lambda do |acceptor|
55
+ acceptor.on_accept do |connection|
56
+ connection.on_data do |data|
57
+ connection.write(data)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ let :port do
64
+ 2**15 + rand(2**15)
65
+ end
66
+
67
+ before do
68
+ io_reactor.start.value
69
+ end
70
+
71
+ after do
72
+ io_reactor.stop.value
73
+ end
74
+
75
+ it 'starts a server' do
76
+ io_reactor.bind(ENV['SERVER_HOST'], port, 1, &protocol_handler_factory).value
77
+ end
78
+
79
+ it 'starts a server that listens to the specified port' do
80
+ io_reactor.bind(ENV['SERVER_HOST'], port, 1, &protocol_handler_factory).value
81
+ socket = TCPSocket.new(ENV['SERVER_HOST'], port)
82
+ socket.puts('HELLO')
83
+ result = socket.read(5)
84
+ result.should == 'HELLO'
85
+ socket.close
86
+ end
87
+ end
51
88
  end
52
89
 
53
90
  module IoSpec