cql-rb 1.0.6 → 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.
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