cql-rb 1.0.6 → 1.1.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/README.md +4 -9
  2. data/lib/cql.rb +1 -0
  3. data/lib/cql/byte_buffer.rb +23 -7
  4. data/lib/cql/client.rb +11 -6
  5. data/lib/cql/client/asynchronous_client.rb +37 -83
  6. data/lib/cql/client/asynchronous_prepared_statement.rb +10 -4
  7. data/lib/cql/client/column_metadata.rb +16 -0
  8. data/lib/cql/client/request_runner.rb +46 -0
  9. data/lib/cql/future.rb +4 -5
  10. data/lib/cql/io.rb +2 -5
  11. data/lib/cql/io/connection.rb +220 -0
  12. data/lib/cql/io/io_reactor.rb +213 -185
  13. data/lib/cql/protocol.rb +1 -0
  14. data/lib/cql/protocol/cql_protocol_handler.rb +201 -0
  15. data/lib/cql/protocol/decoding.rb +6 -31
  16. data/lib/cql/protocol/encoding.rb +1 -5
  17. data/lib/cql/protocol/request.rb +4 -0
  18. data/lib/cql/protocol/responses/schema_change_result_response.rb +15 -0
  19. data/lib/cql/protocol/type_converter.rb +56 -76
  20. data/lib/cql/time_uuid.rb +104 -0
  21. data/lib/cql/uuid.rb +4 -2
  22. data/lib/cql/version.rb +1 -1
  23. data/spec/cql/client/asynchronous_client_spec.rb +47 -71
  24. data/spec/cql/client/asynchronous_prepared_statement_spec.rb +68 -0
  25. data/spec/cql/client/client_shared.rb +3 -3
  26. data/spec/cql/client/column_metadata_spec.rb +80 -0
  27. data/spec/cql/client/request_runner_spec.rb +120 -0
  28. data/spec/cql/future_spec.rb +26 -11
  29. data/spec/cql/io/connection_spec.rb +460 -0
  30. data/spec/cql/io/io_reactor_spec.rb +212 -265
  31. data/spec/cql/protocol/cql_protocol_handler_spec.rb +216 -0
  32. data/spec/cql/protocol/decoding_spec.rb +9 -28
  33. data/spec/cql/protocol/encoding_spec.rb +0 -5
  34. data/spec/cql/protocol/request_spec.rb +16 -0
  35. data/spec/cql/protocol/response_frame_spec.rb +2 -2
  36. data/spec/cql/protocol/responses/schema_change_result_response_spec.rb +70 -0
  37. data/spec/cql/time_uuid_spec.rb +136 -0
  38. data/spec/cql/uuid_spec.rb +1 -5
  39. data/spec/integration/client_spec.rb +34 -38
  40. data/spec/integration/io_spec.rb +283 -0
  41. data/spec/integration/protocol_spec.rb +53 -113
  42. data/spec/integration/regression_spec.rb +124 -0
  43. data/spec/integration/uuid_spec.rb +76 -0
  44. data/spec/spec_helper.rb +12 -9
  45. data/spec/support/fake_io_reactor.rb +52 -21
  46. data/spec/support/fake_server.rb +2 -2
  47. metadata +33 -10
  48. checksums.yaml +0 -15
  49. data/lib/cql/io/node_connection.rb +0 -209
  50. data/spec/cql/protocol/type_converter_spec.rb +0 -52
data/lib/cql/future.rb CHANGED
@@ -62,7 +62,7 @@ module Cql
62
62
  raise FutureError, 'Future already completed' if complete? || failed?
63
63
  @value = v
64
64
  @complete_listeners.each do |listener|
65
- listener.call(@value)
65
+ listener.call(@value) rescue nil
66
66
  end
67
67
  end
68
68
  ensure
@@ -84,7 +84,7 @@ module Cql
84
84
  def on_complete(&listener)
85
85
  @state_lock.synchronize do
86
86
  if complete?
87
- listener.call(value)
87
+ listener.call(value) rescue nil
88
88
  else
89
89
  @complete_listeners << listener
90
90
  end
@@ -101,7 +101,6 @@ module Cql
101
101
  raise @error if @error
102
102
  return @value if defined? @value
103
103
  @value_barrier.pop
104
- @value_barrier << :ping
105
104
  raise @error if @error
106
105
  return @value
107
106
  end
@@ -118,7 +117,7 @@ module Cql
118
117
  raise FutureError, 'Future already completed' if failed? || complete?
119
118
  @error = error
120
119
  @failure_listeners.each do |listener|
121
- listener.call(error)
120
+ listener.call(error) rescue nil
122
121
  end
123
122
  end
124
123
  ensure
@@ -140,7 +139,7 @@ module Cql
140
139
  def on_failure(&listener)
141
140
  @state_lock.synchronize do
142
141
  if failed?
143
- listener.call(@error)
142
+ listener.call(@error) rescue nil
144
143
  else
145
144
  @failure_listeners << listener
146
145
  end
data/lib/cql/io.rb CHANGED
@@ -5,13 +5,10 @@ module Cql
5
5
 
6
6
  module Io
7
7
  ConnectionError = Class.new(IoError)
8
- ConnectionClosedError = Class.new(IoError)
8
+ ConnectionClosedError = Class.new(ConnectionError)
9
9
  ConnectionTimeoutError = Class.new(ConnectionError)
10
- NotRunningError = Class.new(CqlError)
11
- ConnectionNotFoundError = Class.new(CqlError)
12
- ConnectionBusyError = Class.new(CqlError)
13
10
  end
14
11
  end
15
12
 
16
13
  require 'cql/io/io_reactor'
17
- require 'cql/io/node_connection'
14
+ require 'cql/io/connection'
@@ -0,0 +1,220 @@
1
+ # encoding: utf-8
2
+
3
+ require 'socket'
4
+
5
+
6
+ module Cql
7
+ module Io
8
+ # A wrapper around a socket. Handles connecting to the remote host, reading
9
+ # from and writing to the socket.
10
+ #
11
+ class Connection
12
+ attr_reader :host, :port, :connection_timeout
13
+
14
+ # @private
15
+ def initialize(host, port, connection_timeout, unblocker, socket_impl=Socket, clock=Time)
16
+ @host = host
17
+ @port = port
18
+ @connection_timeout = connection_timeout
19
+ @unblocker = unblocker
20
+ @socket_impl = socket_impl
21
+ @clock = clock
22
+ @lock = Mutex.new
23
+ @connected = false
24
+ @write_buffer = ByteBuffer.new
25
+ @connected_future = Future.new
26
+ end
27
+
28
+ # @private
29
+ def connect
30
+ begin
31
+ unless @addrinfos
32
+ @connection_started_at = @clock.now
33
+ @addrinfos = @socket_impl.getaddrinfo(@host, @port, nil, Socket::SOCK_STREAM)
34
+ end
35
+ unless @io
36
+ _, port, _, ip, address_family, socket_type = @addrinfos.shift
37
+ @sockaddr = @socket_impl.sockaddr_in(port, ip)
38
+ @io = @socket_impl.new(address_family, socket_type, 0)
39
+ end
40
+ unless connected?
41
+ @io.connect_nonblock(@sockaddr)
42
+ @connected = true
43
+ @connected_future.complete!(self)
44
+ end
45
+ rescue Errno::EISCONN
46
+ @connected = true
47
+ @connected_future.complete!(self)
48
+ rescue Errno::EINPROGRESS, Errno::EALREADY
49
+ if @clock.now - @connection_started_at > @connection_timeout
50
+ close(ConnectionTimeoutError.new("Could not connect to #{@host}:#{@port} within #{@connection_timeout}s"))
51
+ end
52
+ rescue Errno::EINVAL => e
53
+ if @addrinfos.empty?
54
+ close(e)
55
+ else
56
+ @io = nil
57
+ retry
58
+ end
59
+ rescue SystemCallError => e
60
+ close(e)
61
+ rescue SocketError => e
62
+ close(e) || closed!(e)
63
+ end
64
+ @connected_future
65
+ end
66
+
67
+ # Closes the connection
68
+ def close(cause=nil)
69
+ return false unless @io
70
+ begin
71
+ @io.close
72
+ rescue SystemCallError, IOError
73
+ # nothing to do, the socket was most likely already closed
74
+ end
75
+ closed!(cause)
76
+ true
77
+ end
78
+
79
+ # @private
80
+ def connecting?
81
+ !(closed? || connected?)
82
+ end
83
+
84
+ # Returns true if the connection is connected
85
+ def connected?
86
+ @connected
87
+ end
88
+
89
+ # Returns true if the connection is closed
90
+ def closed?
91
+ @io.nil?
92
+ end
93
+
94
+ # @private
95
+ def writable?
96
+ empty_buffer = @lock.synchronize do
97
+ @write_buffer.empty?
98
+ end
99
+ !(closed? || empty_buffer)
100
+ end
101
+
102
+ # Register to receive notifications when new data is read from the socket.
103
+ #
104
+ # You should only call this method in your protocol handler constructor.
105
+ #
106
+ # Only one callback can be registered, if you register multiple times only
107
+ # the last one will receive notifications. This is not meant as a general
108
+ # event system, it's just for protocol handlers to receive data from their
109
+ # connection. If you want multiple listeners you need to implement that
110
+ # yourself in your protocol handler.
111
+ #
112
+ # It is very important that you don't do any heavy lifting in the callback
113
+ # since it is called from the IO reactor thread, and as long as the
114
+ # callback is working the reactor can't handle any IO and no other
115
+ # callbacks can be called.
116
+ #
117
+ # Errors raised by the callback will be ignored.
118
+ #
119
+ # @yield [String] the new data
120
+ #
121
+ def on_data(&listener)
122
+ @data_listener = listener
123
+ end
124
+
125
+ # Register to receive a notification when the socket is closed, both for
126
+ # expected and unexpected reasons.
127
+ #
128
+ # You shoud only call this method in your protocol handler constructor.
129
+ #
130
+ # Only one callback can be registered, if you register multiple times only
131
+ # the last one will receive notifications. This is not meant as a general
132
+ # event system, it's just for protocol handlers to be notified of the
133
+ # connection closing. If you want multiple listeners you need to implement
134
+ # that yourself in your protocol handler.
135
+ #
136
+ # Errors raised by the callback will be ignored.
137
+ #
138
+ # @yield [error, nil] the error that caused the socket to close, or nil if
139
+ # the socket closed with #close
140
+ #
141
+ def on_closed(&listener)
142
+ @closed_listener = listener
143
+ end
144
+
145
+ # Write bytes to the socket.
146
+ #
147
+ # You can either pass in bytes (as a string or as a `ByteBuffer`), or you
148
+ # can use the block form of this method to get access to the connection's
149
+ # internal buffer.
150
+ #
151
+ # @yieldparam buffer [Cql::ByteBuffer] the connection's internal buffer
152
+ # @param bytes [String, Cql::ByteBuffer] the data to write to the socket
153
+ #
154
+ def write(bytes=nil)
155
+ @lock.synchronize do
156
+ if block_given?
157
+ yield @write_buffer
158
+ elsif bytes
159
+ @write_buffer.append(bytes)
160
+ end
161
+ end
162
+ @unblocker.unblock!
163
+ end
164
+
165
+ # @private
166
+ def flush
167
+ if writable?
168
+ @lock.synchronize do
169
+ s = @write_buffer.cheap_peek.dup
170
+ bytes_written = @io.write_nonblock(@write_buffer.cheap_peek)
171
+ @write_buffer.discard(bytes_written)
172
+ end
173
+ end
174
+ rescue => e
175
+ close(e)
176
+ end
177
+
178
+ # @private
179
+ def read
180
+ new_data = @io.read_nonblock(2**16)
181
+ @data_listener.call(new_data) if @data_listener
182
+ rescue => e
183
+ close(e)
184
+ end
185
+
186
+ # @private
187
+ def to_io
188
+ @io
189
+ end
190
+
191
+ def to_s
192
+ state = 'inconsistent'
193
+ if connected?
194
+ state = 'connected'
195
+ elsif connecting?
196
+ state = 'connecting'
197
+ elsif closed?
198
+ state = 'closed'
199
+ end
200
+ %(#<#{self.class.name} #{state} #{@host}:#{@port}>)
201
+ end
202
+
203
+ private
204
+
205
+ def closed!(cause)
206
+ @io = nil
207
+ if cause && !cause.is_a?(IoError)
208
+ cause = ConnectionError.new(cause.message)
209
+ end
210
+ unless connected?
211
+ @connected_future.fail!(cause)
212
+ end
213
+ @connected = false
214
+ if @closed_listener
215
+ @closed_listener.call(cause)
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -2,54 +2,145 @@
2
2
 
3
3
  module Cql
4
4
  module Io
5
- # An instance of IO reactor manages the connections used by a client.
5
+ ReactorError = Class.new(IoError)
6
+
7
+ # An IO reactor takes care of all the IO for a client. It handles opening
8
+ # new connections, and making sure that connections that have data to send
9
+ # flush to the network, and connections that have data coming in read that
10
+ # data and delegate it to their protocol handlers.
11
+ #
12
+ # All IO is done in a single background thread, regardless of how many
13
+ # connections you open. There shouldn't be any problems handling hundreds of
14
+ # connections if needed. All operations are thread safe, but you should take
15
+ # great care when in your protocol handlers to make sure that they don't
16
+ # do too much work in their data handling callbacks, since those will be
17
+ # run in the reactor thread, and every cycle you use there is a cycle which
18
+ # can't be used to handle IO.
19
+ #
20
+ # The IO reactor is completely protocol agnostic, and it's up to the
21
+ # specified protocol handler factory to create objects that can interpret
22
+ # the bytes received from remote hosts, and to send the correct commands
23
+ # back. The way this works is that when you create an IO reactor you provide
24
+ # a factory that can create protocol handler objects (this factory is most
25
+ # of the time just class, but it could potentially be any object that
26
+ # responds to #new). When you #connect a new protocol handler instance is
27
+ # created and passed a connection. The protocol handler can then register to
28
+ # receive data that arrives over the socket, and it can write data to the
29
+ # socket. It can also register to be notified when the socket is closed, or
30
+ # it can itself close the socket.
31
+ #
32
+ # @example A protocol handler that processes whole lines
33
+ #
34
+ # class LineProtocolHandler
35
+ # def initialize(connection)
36
+ # @connection = connection
37
+ # # register a listener method for new data, this must be done in the
38
+ # # in the constructor, and only one listener can be registered
39
+ # @connection.on_data(&method(:process_data))
40
+ # @buffer = ''
41
+ # end
42
+ #
43
+ # def process_data(new_data)
44
+ # # in this fictional protocol we want to process whole lines, so we
45
+ # # append new data to our buffer and then loop as long as there is
46
+ # # a newline in the buffer, everything up until a newline is a
47
+ # # complete line
48
+ # @buffer << new_data
49
+ # while newline_index = @buffer.index("\n")
50
+ # line = @buffer.slice!(0, newline_index + 1)
51
+ # line.chomp!
52
+ # # Now do something interesting with the line, but remember that
53
+ # # while you're in the data listener method you're executing in the
54
+ # # IO reactor thread so you're blocking the reactor from doing
55
+ # # other IO work. You should not do any heavy lifting here, but
56
+ # # instead hand off the data to your application's other threads.
57
+ # # One way of doing that is to create a Cql::Future in the method
58
+ # # that sends the request, and then complete the future in this
59
+ # # method. How you keep track of which future belongs to which
60
+ # # reply is very protocol dependent so you'll have to figure that
61
+ # # out yourself.
62
+ # end
63
+ # end
6
64
  #
7
- # The reactor starts a thread in which all IO is performed. The IO reactor
8
- # instances are thread safe.
65
+ # def send_request(command_string)
66
+ # # This example primarily shows how to implement a data listener
67
+ # # method, but this is how you write data to the connection. The
68
+ # # method can be called anything, it doesn't have to be #send_request
69
+ # @connection.write(command_string)
70
+ # # The connection object itself is threadsafe, but to create any
71
+ # # interesting protocol you probably need to set up some state for
72
+ # # each request so that you know which request to complete when you
73
+ # # get data back.
74
+ # end
75
+ # end
76
+ #
77
+ # See {Cql::Protocol::CqlProtocolHandler} for an example of how the CQL
78
+ # protocol is implemented, and there is an integration tests that implements
79
+ # the Redis protocol that you can look at too.
9
80
  #
10
81
  class IoReactor
82
+ # Initializes a new IO reactor.
11
83
  #
12
- # @param [Hash] options
13
- # @option options [Integer] :connection_timeout (5) Max time to wait for a
14
- # connection, in seconds
84
+ # @param protocol_handler_factory [Object] a class that will be used
85
+ # create the protocol handler objects returned by {#connect}
86
+ # @param options [Hash] only used to inject behaviour during tests
15
87
  #
16
- def initialize(options={})
17
- @connection_timeout = options[:connection_timeout] || 5
18
- @lock = Mutex.new
19
- @command_queue = []
20
- @unblocker = UnblockerConnection.new(*IO.pipe)
21
- @connections = [@unblocker]
88
+ def initialize(protocol_handler_factory, options={})
89
+ @protocol_handler_factory = protocol_handler_factory
90
+ @unblocker = Unblocker.new
91
+ @io_loop = IoLoopBody.new(options)
92
+ @io_loop.add_socket(@unblocker)
93
+ @running = false
94
+ @stopped = false
22
95
  @started_future = Future.new
23
96
  @stopped_future = Future.new
24
- @running = false
97
+ @lock = Mutex.new
25
98
  end
26
99
 
27
- # Returns whether or not the reactor is running
100
+ # Register to receive notifications when the reactor shuts down because
101
+ # on an irrecoverable error.
102
+ #
103
+ # The listener block will be called in the reactor thread. Any errors that
104
+ # it raises will be ignored.
105
+ #
106
+ # @yield [error] the error that cause the reactor to stop
107
+ #
108
+ def on_error(&listener)
109
+ @stopped_future.on_failure(&listener)
110
+ end
111
+
112
+ # Returns true as long as the reactor is running. It will be true even
113
+ # after #stop has been called, but false when the future returned by
114
+ # #stop completes.
28
115
  #
29
116
  def running?
30
117
  @running
31
118
  end
32
119
 
33
- # Starts the reactor.
34
- #
35
- # Calling this method when the reactor is connecting or is connected has
36
- # no effect.
120
+ # Starts the reactor. This will spawn a background thread that will manage
121
+ # all connections.
37
122
  #
38
- # @return [Future<nil>] a future which completes when the reactor has started
123
+ # This method is asynchronous and returns a future which completes when
124
+ # the reactor has started.
39
125
  #
126
+ # @return [Cql::Future] a future that will resolve to the reactor itself
40
127
  def start
41
128
  @lock.synchronize do
42
- unless @running
43
- @running = true
44
- @reactor_thread = Thread.start do
45
- begin
46
- @started_future.complete!
47
- io_loop
48
- @stopped_future.complete!
49
- rescue => e
50
- @stopped_future.fail!(e)
51
- raise
52
- end
129
+ raise ReactorError, 'Cannot start a stopped IO reactor' if @stopped
130
+ return @started_future if @running
131
+ @running = true
132
+ end
133
+ Thread.start do
134
+ @started_future.complete!(self)
135
+ begin
136
+ @io_loop.tick until @stopped
137
+ ensure
138
+ @io_loop.close_sockets
139
+ @running = false
140
+ if $!
141
+ @stopped_future.fail!($!)
142
+ else
143
+ @stopped_future.complete!(self)
53
144
  end
54
145
  end
55
146
  end
@@ -58,208 +149,145 @@ module Cql
58
149
 
59
150
  # Stops the reactor.
60
151
  #
61
- # Calling this method when the reactor is stopping or has stopped has
62
- # no effect.
152
+ # This method is asynchronous and returns a future which completes when
153
+ # the reactor has completely stopped, or fails with an error if the reactor
154
+ # stops or has already stopped because of a failure.
63
155
  #
64
- # @return [Future<nil>] a future which completes when the reactor has stopped
156
+ # @return [Cql::Future] a future that will resolve to the reactor itself
65
157
  #
66
158
  def stop
67
- @running = false
68
- command_queue_push(nil)
159
+ @stopped = true
69
160
  @stopped_future
70
161
  end
71
162
 
72
- # Establish a new connection.
163
+ # Opens a connection to the specified host and port.
73
164
  #
74
- # @param [String] host The hostname to connect to
75
- # @param [Integer] port The port to connect to
76
- # @return [Future<Object>] a future representing the ID of the newly
77
- # established connection, or connection error if the connection fails.
165
+ # This method is asynchronous and returns a future which completes when
166
+ # the connection has been established, or fails if the connection cannot
167
+ # be established for some reason (the connection takes longer than the
168
+ # specified timeout, the remote host cannot be found, etc.).
78
169
  #
79
- def add_connection(host, port)
80
- connection = NodeConnection.new(host, port, @connection_timeout)
81
- connection.on_close do
82
- @lock.synchronize do
83
- @connections.delete(connection)
84
- connection_commands, @command_queue = @command_queue.partition do |command|
85
- command.is_a?(TargetedRequestCommand) && command.connection_id == connection.connection_id
86
- end
87
- connection_commands.each do |command|
88
- command.future.fail!(ConnectionClosedError.new)
89
- end
90
- end
91
- end
92
- f = connection.open
93
- @lock.synchronize do
94
- @connections << connection
95
- end
96
- command_queue_push(nil)
97
- f
98
- end
99
-
100
- # Sends a request over a random, or specific connection.
170
+ # The object returned in the future will be an instance of the protocol
171
+ # handler class you passed to {#initialize}.
101
172
  #
102
- # @param [Cql::Protocol::Request] request the request to send
103
- # @param [Object] connection_id the ID of the connection which should be
104
- # used to send the request
105
- # @return [Future<ResultResponse>] a future representing the result of the request
173
+ # @param host [String] the host to connect to
174
+ # @param port [Integer] the port to connect to
175
+ # @param timeout [Numeric] the number of seconds to wait for a connection
176
+ # before failing
177
+ # @return [Cql::Future] a future that will resolve to a protocol handler
178
+ # object that will be your interface to interact with the connection
106
179
  #
107
- def queue_request(request, connection_id=nil)
108
- command = connection_id ? TargetedRequestCommand.new(request, connection_id) : RequestCommand.new(request)
109
- command_queue_push(command)
110
- command.future
180
+ def connect(host, port, timeout)
181
+ connection = Connection.new(host, port, timeout, @unblocker)
182
+ f = connection.connect
183
+ protocol_handler = @protocol_handler_factory.new(connection)
184
+ @io_loop.add_socket(connection)
185
+ @unblocker.unblock!
186
+ f.map { protocol_handler }
111
187
  end
112
188
 
113
- # Registers a listener to receive server sent events.
114
- #
115
- # @yieldparam [Cql::Protocol::EventResponse] event the event sent by the server
116
- #
117
- def add_event_listener(&listener)
118
- command_queue_push(EventListenerCommand.new(listener))
189
+ def to_s
190
+ @io_loop.to_s
119
191
  end
192
+ end
120
193
 
121
- private
122
-
123
- def io_loop
124
- while running?
125
- read_ready_streams = @connections.select(&:connected?)
126
- write_ready_streams = @connections.select(&:can_write?)
127
- readables, writables, _ = IO.select(read_ready_streams, write_ready_streams, nil, 1)
128
- readables && readables.each(&:handle_read)
129
- writables && writables.each(&:handle_write)
130
- @connections.each do |connection|
131
- if connection.connecting?
132
- connection.handle_connecting
133
- end
134
- end
135
- if running?
136
- perform_queued_commands
137
- end
138
- end
139
- ensure
140
- stop
141
- @connections.dup.each do |connection|
142
- begin
143
- connection.close
144
- rescue IOError => e
145
- end
146
- end
194
+ # @private
195
+ class Unblocker
196
+ def initialize
197
+ @out, @in = IO.pipe
198
+ @lock = Mutex.new
147
199
  end
148
200
 
149
- def command_queue_push(command)
150
- if command
151
- @lock.synchronize do
152
- @command_queue << command
153
- end
154
- end
155
- @unblocker.unblock!
201
+ def connected?
202
+ true
156
203
  end
157
204
 
158
- def perform_queued_commands
159
- @lock.synchronize do
160
- unexecuted_commands = []
161
- while (command = @command_queue.shift)
162
- case command
163
- when EventListenerCommand
164
- @connections.each do |connection|
165
- connection.on_event(&command.listener)
166
- end
167
- when TargetedRequestCommand
168
- connection = @connections.find { |c| c.connection_id == command.connection_id }
169
- if connection && connection.connected? && connection.has_capacity?
170
- connection.perform_request(command.request, command.future)
171
- elsif connection && connection.connected?
172
- unexecuted_commands << command
173
- else
174
- command.future.fail!(ConnectionNotFoundError.new("Connection ##{command.connection_id} does not exist"))
175
- end
176
- when RequestCommand
177
- connection = @connections.select(&:has_capacity?).sample
178
- if connection
179
- connection.perform_request(command.request, command.future)
180
- else
181
- unexecuted_commands << command
182
- end
183
- end
184
- end
185
- @command_queue = unexecuted_commands
186
- end
205
+ def connecting?
206
+ false
187
207
  end
188
- end
189
-
190
- class EventListenerCommand
191
- attr_reader :listener
192
208
 
193
- def initialize(listener)
194
- @listener = listener
209
+ def writable?
210
+ false
195
211
  end
196
- end
197
-
198
- class RequestCommand
199
- attr_reader :future, :request
200
212
 
201
- def initialize(request)
202
- @request = request
203
- @future = Future.new
213
+ def closed?
214
+ @in.nil?
204
215
  end
205
- end
206
-
207
- class TargetedRequestCommand < RequestCommand
208
- attr_reader :connection_id
209
216
 
210
- def initialize(request, connection_id)
211
- super(request)
212
- @connection_id = connection_id
217
+ def unblock!
218
+ @lock.synchronize do
219
+ @in.write(PING_BYTE)
220
+ end
213
221
  end
214
- end
215
222
 
216
- class UnblockerConnection
217
- def initialize(*args)
218
- @out, @in = args
223
+ def read
224
+ @out.read_nonblock(2**16)
219
225
  end
220
226
 
221
- def unblock!
222
- @in.write(PING_BYTE)
227
+ def close
228
+ @in.close
229
+ @out.close
230
+ @in = nil
231
+ @out = nil
223
232
  end
224
233
 
225
234
  def to_io
226
235
  @out
227
236
  end
228
237
 
229
- def close
238
+ def to_s
239
+ %(#<#{self.class.name}>)
230
240
  end
231
241
 
232
- def on_event; end
233
-
234
- def on_close; end
242
+ private
235
243
 
236
- def connection_id
237
- -1
238
- end
244
+ PING_BYTE = "\0".freeze
245
+ end
239
246
 
240
- def connected?
241
- true
247
+ # @private
248
+ class IoLoopBody
249
+ def initialize(options={})
250
+ @selector = options[:selector] || IO
251
+ @lock = Mutex.new
252
+ @sockets = []
242
253
  end
243
254
 
244
- def connecting?
245
- false
255
+ def add_socket(socket)
256
+ @lock.synchronize do
257
+ sockets = @sockets.dup
258
+ sockets << socket
259
+ @sockets = sockets
260
+ end
246
261
  end
247
262
 
248
- def can_write?
249
- false
263
+ def close_sockets
264
+ @sockets.each do |s|
265
+ begin
266
+ s.close unless s.closed?
267
+ rescue
268
+ # the socket had most likely already closed due to an error
269
+ end
270
+ end
250
271
  end
251
272
 
252
- def has_capacity?
253
- false
273
+ def tick(timeout=1)
274
+ readables, writables, connecting = [], [], []
275
+ sockets = @sockets
276
+ sockets.reject! { |s| s.closed? }
277
+ sockets.each do |s|
278
+ readables << s if s.connected?
279
+ writables << s if s.connecting? || s.writable?
280
+ connecting << s if s.connecting?
281
+ end
282
+ r, w, _ = @selector.select(readables, writables, nil, timeout)
283
+ connecting.each(&:connect)
284
+ r && r.each(&:read)
285
+ w && w.each(&:flush)
254
286
  end
255
287
 
256
- def handle_read
257
- @out.read_nonblock(2**16)
288
+ def to_s
289
+ %(#<#{IoReactor.name} @connections=[#{@sockets.map(&:to_s).join(', ')}]>)
258
290
  end
259
-
260
- private
261
-
262
- PING_BYTE = "\0".freeze
263
291
  end
264
292
  end
265
293
  end