cql-rb 1.1.0.pre3 → 1.1.0.pre6

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 (42) hide show
  1. data/README.md +2 -2
  2. data/lib/cql/client.rb +9 -5
  3. data/lib/cql/client/asynchronous_client.rb +105 -192
  4. data/lib/cql/client/asynchronous_prepared_statement.rb +51 -9
  5. data/lib/cql/client/connection_helper.rb +155 -0
  6. data/lib/cql/client/connection_manager.rb +56 -0
  7. data/lib/cql/client/keyspace_changer.rb +27 -0
  8. data/lib/cql/client/null_logger.rb +21 -0
  9. data/lib/cql/client/request_runner.rb +5 -3
  10. data/lib/cql/client/synchronous_client.rb +5 -5
  11. data/lib/cql/client/synchronous_prepared_statement.rb +4 -8
  12. data/lib/cql/future.rb +320 -210
  13. data/lib/cql/io/connection.rb +5 -5
  14. data/lib/cql/io/io_reactor.rb +21 -23
  15. data/lib/cql/protocol/cql_protocol_handler.rb +69 -38
  16. data/lib/cql/protocol/encoding.rb +5 -1
  17. data/lib/cql/protocol/requests/register_request.rb +2 -0
  18. data/lib/cql/protocol/type_converter.rb +1 -0
  19. data/lib/cql/version.rb +1 -1
  20. data/spec/cql/client/asynchronous_client_spec.rb +368 -175
  21. data/spec/cql/client/asynchronous_prepared_statement_spec.rb +132 -22
  22. data/spec/cql/client/connection_helper_spec.rb +335 -0
  23. data/spec/cql/client/connection_manager_spec.rb +118 -0
  24. data/spec/cql/client/keyspace_changer_spec.rb +50 -0
  25. data/spec/cql/client/request_runner_spec.rb +12 -12
  26. data/spec/cql/client/synchronous_client_spec.rb +15 -15
  27. data/spec/cql/client/synchronous_prepared_statement_spec.rb +15 -11
  28. data/spec/cql/future_spec.rb +529 -301
  29. data/spec/cql/io/connection_spec.rb +12 -12
  30. data/spec/cql/io/io_reactor_spec.rb +61 -61
  31. data/spec/cql/protocol/cql_protocol_handler_spec.rb +26 -12
  32. data/spec/cql/protocol/encoding_spec.rb +5 -0
  33. data/spec/cql/protocol/type_converter_spec.rb +1 -1
  34. data/spec/cql/time_uuid_spec.rb +7 -7
  35. data/spec/integration/client_spec.rb +2 -2
  36. data/spec/integration/io_spec.rb +20 -20
  37. data/spec/integration/protocol_spec.rb +17 -17
  38. data/spec/integration/regression_spec.rb +6 -0
  39. data/spec/integration/uuid_spec.rb +4 -0
  40. data/spec/support/fake_io_reactor.rb +38 -8
  41. data/spec/support/fake_server.rb +3 -3
  42. metadata +12 -2
data/README.md CHANGED
@@ -118,7 +118,7 @@ A prepared statement can be run many times, but the CQL parsing will only be don
118
118
 
119
119
  At this time prepared statements are local to a single connection. Even if you connect to multiple nodes a prepared statement is only ever going to be executed against one of the nodes.
120
120
 
121
- # Consistency levels
121
+ # Consistency
122
122
 
123
123
  The `#execute` (of `Client` and `PreparedStatement`) method supports setting the desired consistency level for the statement:
124
124
 
@@ -139,7 +139,7 @@ The possible values are:
139
139
 
140
140
  The default consistency level is `:quorum`.
141
141
 
142
- Consistency level is ignored for `USE`, `TRUNCATE`, `CREATE` and `ALTER` statements, and some (like `:any`) aren't allowed in all situations.
142
+ Consistency is ignored for `USE`, `TRUNCATE`, `CREATE` and `ALTER` statements, and some (like `:any`) aren't allowed in all situations.
143
143
 
144
144
  ## CQL3
145
145
 
@@ -147,13 +147,13 @@ module Cql
147
147
  # Execute the prepared statement with a list of values for the bound parameters.
148
148
  #
149
149
  # The number of arguments must equal the number of bound parameters.
150
- # To set the consistency level for the request you pass a consistency
151
- # level (as a symbol) as the last argument. Needless to say, if you pass
152
- # the value for one bound parameter too few, and then a consistency level,
153
- # or if you pass too many values, you will get weird errors.
150
+ # To set the consistency for the request you pass a consistency (as a
151
+ # symbol) as the last argument. Needless to say, if you pass the value for
152
+ # one bound parameter too few, and then a consistency, or if you pass too
153
+ # many values, you will get weird errors.
154
154
  #
155
155
  # @param args [Array] the values for the bound parameters, and optionally
156
- # the desired consistency level, as a symbol (defaults to :quorum)
156
+ # the desired consistency, as a symbol (defaults to :quorum)
157
157
  # @raise [Cql::NotConnectedError] raised when the client is not connected
158
158
  # @raise [Cql::QueryError] raised when there is an error on the server side
159
159
  # @return [nil, Cql::Client::QueryResult] Most statements have no result and return
@@ -165,9 +165,13 @@ module Cql
165
165
  end
166
166
  end
167
167
 
168
+ require 'cql/client/connection_manager'
169
+ require 'cql/client/connection_helper'
170
+ require 'cql/client/null_logger'
168
171
  require 'cql/client/column_metadata'
169
172
  require 'cql/client/result_metadata'
170
173
  require 'cql/client/query_result'
174
+ require 'cql/client/keyspace_changer'
171
175
  require 'cql/client/asynchronous_client'
172
176
  require 'cql/client/asynchronous_prepared_statement'
173
177
  require 'cql/client/synchronous_client'
@@ -5,41 +5,45 @@ module Cql
5
5
  # @private
6
6
  class AsynchronousClient < Client
7
7
  def initialize(options={})
8
- @connection_timeout = options[:connection_timeout] || 10
9
- @hosts = extract_hosts(options)
10
- @port = options[:port] || 9042
8
+ @logger = options[:logger] || NullLogger.new
11
9
  @io_reactor = options[:io_reactor] || Io::IoReactor.new(Protocol::CqlProtocolHandler)
10
+ @hosts = extract_hosts(options)
11
+ @initial_keyspace = options[:keyspace]
12
+ @default_consistency = options[:default_consistency] || DEFAULT_CONSISTENCY
12
13
  @lock = Mutex.new
13
14
  @connected = false
14
15
  @connecting = false
15
16
  @closing = false
16
- @initial_keyspace = options[:keyspace]
17
- @credentials = options[:credentials]
18
17
  @request_runner = RequestRunner.new
18
+ @keyspace_changer = KeyspaceChanger.new
19
+ @connection_manager = ConnectionManager.new
20
+ port = options[:port] || DEFAULT_PORT
21
+ credentials = options[:credentials]
22
+ connection_timeout = options[:connection_timeout] || DEFAULT_CONNECTION_TIMEOUT
23
+ @connection_helper = ConnectionHelper.new(@io_reactor, port, credentials, connection_timeout, @logger)
19
24
  end
20
25
 
21
26
  def connect
22
27
  @lock.synchronize do
23
28
  return @connected_future if can_execute?
24
29
  @connecting = true
25
- @connected_future = Future.new
26
- @connections = []
27
- end
28
- when_not_closing do
29
- setup_connections
30
- end
31
- @connected_future.on_complete do
32
- @lock.synchronize do
33
- @connecting = false
34
- @connected = true
35
- end
36
- end
37
- @connected_future.on_failure do
38
- @lock.synchronize do
39
- @connecting = false
40
- @connected = false
30
+ @connected_future = begin
31
+ f = @connection_helper.connect(@hosts, @initial_keyspace)
32
+ if @closing
33
+ ff = @closed_future
34
+ ff = ff.flat_map { f }
35
+ ff = ff.fallback { f }
36
+ else
37
+ ff = f
38
+ end
39
+ ff.on_value do |connections|
40
+ @connection_manager.add_connections(connections)
41
+ register_event_listener(@connection_manager.random_connection)
42
+ end
43
+ ff.map { self }
41
44
  end
42
45
  end
46
+ @connected_future.on_complete(&method(:connected))
43
47
  @connected_future
44
48
  end
45
49
 
@@ -47,25 +51,19 @@ module Cql
47
51
  @lock.synchronize do
48
52
  return @closed_future if @closing
49
53
  @closing = true
50
- @closed_future = Future.new
51
- end
52
- when_not_connecting do
53
- f = @io_reactor.stop
54
- f.on_complete { @closed_future.complete!(self) }
55
- f.on_failure { |e| @closed_future.fail!(e) }
56
- end
57
- @closed_future.on_complete do
58
- @lock.synchronize do
59
- @closing = false
60
- @connected = false
61
- end
62
- end
63
- @closed_future.on_failure do
64
- @lock.synchronize do
65
- @closing = false
66
- @connected = false
54
+ @closed_future = begin
55
+ f = @io_reactor.stop
56
+ if @connecting
57
+ ff = @connected_future
58
+ ff = f.flat_map { ff }
59
+ ff = f.fallback { ff }
60
+ else
61
+ ff = f
62
+ end
63
+ ff.map { self }
67
64
  end
68
65
  end
66
+ @closed_future.on_complete(&method(:closed))
69
67
  @closed_future
70
68
  end
71
69
 
@@ -74,55 +72,38 @@ module Cql
74
72
  end
75
73
 
76
74
  def keyspace
77
- @lock.synchronize do
78
- @connections.first.keyspace
79
- end
75
+ @connection_manager.random_connection.keyspace
80
76
  end
81
77
 
82
78
  def use(keyspace)
83
79
  with_failure_handler do
84
- connections = @lock.synchronize do
85
- @connections.select { |c| c.keyspace != keyspace }
86
- end
80
+ connections = @connection_manager.select { |c| c.keyspace != keyspace }
87
81
  if connections.any?
88
- futures = connections.map { |connection| use_keyspace(keyspace, connection) }
89
- Future.combine(*futures).map { nil }
82
+ futures = connections.map { |connection| @keyspace_changer.use_keyspace(keyspace, connection) }
83
+ Future.all(*futures).map { nil }
90
84
  else
91
- Future.completed(nil)
85
+ Future.resolved
92
86
  end
93
87
  end
94
88
  end
95
89
 
96
90
  def execute(cql, consistency=nil)
97
91
  with_failure_handler do
98
- consistency ||= DEFAULT_CONSISTENCY_LEVEL
99
- execute_request(Protocol::QueryRequest.new(cql, consistency))
92
+ execute_request(Protocol::QueryRequest.new(cql, consistency || @default_consistency))
100
93
  end
101
94
  end
102
95
 
103
96
  def prepare(cql)
104
97
  with_failure_handler do
105
- execute_request(Protocol::PrepareRequest.new(cql))
98
+ AsynchronousPreparedStatement.prepare(cql, @default_consistency, @connection_manager, @logger)
106
99
  end
107
100
  end
108
101
 
109
102
  private
110
103
 
111
- KEYSPACE_NAME_PATTERN = /^\w[\w\d_]*$|^"\w[\w\d_]*"$/
112
- DEFAULT_CONSISTENCY_LEVEL = :quorum
113
- BIND_ALL_IP = '0.0.0.0'.freeze
114
-
115
- class FailedConnection
116
- attr_reader :error
117
-
118
- def initialize(error)
119
- @error = error
120
- end
121
-
122
- def connected?
123
- false
124
- end
125
- end
104
+ DEFAULT_CONSISTENCY = :quorum
105
+ DEFAULT_PORT = 9042
106
+ DEFAULT_CONNECTION_TIMEOUT = 10
126
107
 
127
108
  def extract_hosts(options)
128
109
  if options[:hosts]
@@ -134,159 +115,91 @@ module Cql
134
115
  end
135
116
  end
136
117
 
137
- def can_execute?
138
- @connected || @connecting
139
- end
140
-
141
- def valid_keyspace_name?(name)
142
- name =~ KEYSPACE_NAME_PATTERN
143
- end
144
-
145
- def with_failure_handler
146
- return Future.failed(NotConnectedError.new) unless can_execute?
147
- yield
148
- rescue => e
149
- Future.failed(e)
150
- end
151
-
152
- def when_not_connecting(&callback)
153
- if @connecting
154
- @connected_future.on_complete(&callback)
155
- @connected_future.on_failure(&callback)
156
- else
157
- callback.call
158
- end
159
- end
160
-
161
- def when_not_closing(&callback)
162
- if @closing
163
- @closed_future.on_complete(&callback)
164
- @closed_future.on_failure(&callback)
165
- else
166
- callback.call
167
- end
168
- end
169
-
170
- def discover_peers(seed_connections, initial_keyspace)
171
- connected_seeds = seed_connections.select(&:connected?)
172
- connection = connected_seeds.sample
173
- return Future.completed([]) unless connection
174
- request = Protocol::QueryRequest.new('SELECT peer, data_center, host_id, rpc_address FROM system.peers', :one)
175
- peer_info = execute_request(request, connection)
176
- peer_info.flat_map do |result|
177
- seed_dcs = connected_seeds.map { |c| c[:data_center] }.uniq
178
- unconnected_peers = result.select do |row|
179
- seed_dcs.include?(row['data_center']) && connected_seeds.none? { |c| c[:host_id] == row['host_id'] }
118
+ def connected(f)
119
+ if f.resolved?
120
+ @lock.synchronize do
121
+ @connecting = false
122
+ @connected = true
180
123
  end
181
- node_addresses = unconnected_peers.map do |row|
182
- rpc_address = row['rpc_address'].to_s
183
- if rpc_address == BIND_ALL_IP
184
- row['peer'].to_s
185
- else
186
- rpc_address
187
- end
124
+ @logger.info('Cluster connection complete')
125
+ else
126
+ @lock.synchronize do
127
+ @connecting = false
128
+ @connected = false
188
129
  end
189
- if node_addresses.any?
190
- connect_to_hosts(node_addresses, initial_keyspace, false)
191
- else
192
- Future.completed([])
130
+ f.on_failure do |e|
131
+ @logger.error('Failed connecting to cluster: %s' % e.message)
193
132
  end
133
+ close
194
134
  end
195
135
  end
196
136
 
197
- def setup_connections
198
- f = @io_reactor.start.flat_map do
199
- connect_to_hosts(@hosts, @initial_keyspace, true)
200
- end
201
- f.on_failure do |e|
202
- fail_connecting(e)
203
- end
204
- f.on_complete do |connections|
205
- connected_connections = connections.select(&:connected?)
206
- if connected_connections.any?
207
- @connections = connected_connections
208
- @connected_future.complete!(self)
137
+ def closed(f)
138
+ @lock.synchronize do
139
+ @closing = false
140
+ @connected = false
141
+ if f.resolved?
142
+ @logger.info('Cluster disconnect complete')
209
143
  else
210
- fail_connecting(connections.first.error)
211
- end
212
- end
213
- end
214
-
215
- def fail_connecting(e)
216
- close
217
- if e.is_a?(Cql::QueryError) && e.code == 0x100
218
- @connected_future.fail!(AuthenticationError.new(e.message))
219
- else
220
- @connected_future.fail!(e)
221
- end
222
- end
223
-
224
- def connect_to_hosts(hosts, initial_keyspace, peer_discovery)
225
- connection_futures = hosts.map do |host|
226
- connect_to_host(host, initial_keyspace).recover do |error|
227
- FailedConnection.new(error)
228
- end
229
- end
230
- hosts_connected_future = Future.combine(*connection_futures)
231
- if peer_discovery
232
- hosts_connected_future.flat_map do |connections|
233
- discover_peers(connections, initial_keyspace).map do |peer_connections|
234
- connections + peer_connections
144
+ f.on_failure do |e|
145
+ @logger.error('Cluster disconnect failed: %s' % e.message)
235
146
  end
236
147
  end
237
- else
238
- hosts_connected_future
239
148
  end
240
149
  end
241
150
 
242
- def connect_to_host(host, keyspace)
243
- connected = @io_reactor.connect(host, @port, @connection_timeout)
244
- connected.flat_map do |connection|
245
- initialize_connection(connection, keyspace)
246
- end
151
+ def can_execute?
152
+ !@closing && (@connecting || (@connected && @connection_manager.connected?))
247
153
  end
248
154
 
249
- def initialize_connection(connection, keyspace)
250
- started = execute_request(Protocol::StartupRequest.new, connection)
251
- authenticated = started.flat_map { |response| maybe_authenticate(response, connection) }
252
- identified = authenticated.flat_map { identify_node(connection) }
253
- identified.flat_map { use_keyspace(keyspace, connection) }
155
+ def with_failure_handler
156
+ return Future.failed(NotConnectedError.new) unless can_execute?
157
+ yield
158
+ rescue => e
159
+ Future.failed(e)
254
160
  end
255
161
 
256
- def identify_node(connection)
257
- request = Protocol::QueryRequest.new('SELECT data_center, host_id FROM system.local', :one)
258
- f = execute_request(request, connection)
259
- f.on_complete do |result|
260
- unless result.empty?
261
- connection[:host_id] = result.first['host_id']
262
- connection[:data_center] = result.first['data_center']
162
+ def register_event_listener(connection)
163
+ register_request = Protocol::RegisterRequest.new(Protocol::TopologyChangeEventResponse::TYPE, Protocol::StatusChangeEventResponse::TYPE)
164
+ execute_request(register_request, connection)
165
+ connection.on_closed do
166
+ if connected?
167
+ begin
168
+ register_event_listener(@connection_manager.random_connection)
169
+ rescue NotConnectedError
170
+ # we had started closing down after the connection check
171
+ end
172
+ end
173
+ end
174
+ connection.on_event do |event|
175
+ begin
176
+ if event.change == 'UP'
177
+ @logger.debug('Received UP event')
178
+ handle_topology_change
179
+ end
263
180
  end
264
181
  end
265
- f
266
- end
267
-
268
- def use_keyspace(keyspace, connection)
269
- return Future.completed(connection) unless keyspace
270
- return Future.failed(InvalidKeyspaceNameError.new(%("#{keyspace}" is not a valid keyspace name))) unless valid_keyspace_name?(keyspace)
271
- execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one), connection).map { connection }
272
182
  end
273
183
 
274
- def maybe_authenticate(response, connection)
275
- case response
276
- when AuthenticationRequired
277
- if @credentials
278
- credentials_request = Protocol::CredentialsRequest.new(@credentials)
279
- execute_request(credentials_request, connection).map { connection }
184
+ def handle_topology_change
185
+ seed_connections = @connection_manager.snapshot
186
+ f = @connection_helper.discover_peers(seed_connections, keyspace)
187
+ f.on_value do |connections|
188
+ connected_connections = connections.select(&:connected?)
189
+ if connected_connections.any?
190
+ @connection_manager.add_connections(connected_connections)
280
191
  else
281
- Future.failed(AuthenticationError.new('Server requested authentication, but no credentials given'))
192
+ @logger.debug('Scheduling new peer discovery in 1s')
193
+ f = @io_reactor.schedule_timer(1)
194
+ f.on_value do
195
+ handle_topology_change
196
+ end
282
197
  end
283
- else
284
- Future.completed(connection)
285
198
  end
286
199
  end
287
200
 
288
201
  def execute_request(request, connection=nil)
289
- f = @request_runner.execute(connection || @connections.sample, request)
202
+ f = @request_runner.execute(connection || @connection_manager.random_connection, request)
290
203
  f.map do |result|
291
204
  if result.is_a?(KeyspaceChanged)
292
205
  use(result.keyspace)
@@ -4,22 +4,64 @@ module Cql
4
4
  module Client
5
5
  # @private
6
6
  class AsynchronousPreparedStatement < PreparedStatement
7
- def initialize(connection, statement_id, raw_metadata)
8
- @connection = connection
9
- @statement_id = statement_id
10
- @raw_metadata = raw_metadata
11
- @metadata = ResultMetadata.new(@raw_metadata)
7
+ # @private
8
+ def initialize(cql, default_consistency, connection_manager, logger)
9
+ @cql = cql
10
+ @default_consistency = default_consistency
11
+ @connection_manager = connection_manager
12
+ @logger = logger
12
13
  @request_runner = RequestRunner.new
13
14
  end
14
15
 
16
+ def self.prepare(cql, default_consistency, connection_manager, logger)
17
+ statement = new(cql, default_consistency, connection_manager, logger)
18
+ futures = connection_manager.map do |connection|
19
+ statement.prepare(connection)
20
+ end
21
+ Future.all(*futures).map { statement }
22
+ rescue => e
23
+ Future.failed(e)
24
+ end
25
+
15
26
  def execute(*args)
16
- bound_args = args.shift(@raw_metadata.size)
17
- consistency_level = args.shift || :quorum
18
- request = Cql::Protocol::ExecuteRequest.new(@statement_id, @raw_metadata, bound_args, consistency_level)
19
- @request_runner.execute(@connection, request)
27
+ connection = @connection_manager.random_connection
28
+ if connection[self]
29
+ run(args, connection)
30
+ else
31
+ prepare(connection).flat_map do
32
+ run(args, connection)
33
+ end
34
+ end
20
35
  rescue => e
21
36
  Future.failed(e)
22
37
  end
38
+
39
+ # @private
40
+ def prepare(connection)
41
+ prepare_request = Protocol::PrepareRequest.new(@cql)
42
+ f = @request_runner.execute(connection, prepare_request) do |response|
43
+ connection[self] = response.id
44
+ unless @raw_metadata
45
+ # NOTE: this is not thread safe, but the worst that could happen
46
+ # is that we assign the same data multiple times
47
+ @raw_metadata = response.metadata
48
+ @metadata = ResultMetadata.new(@raw_metadata)
49
+ end
50
+ @logger.debug('Statement prepared on new connection')
51
+ end
52
+ f.map { self }
53
+ end
54
+
55
+ private
56
+
57
+ def run(args, connection)
58
+ statement_id = connection[self]
59
+ bound_args = args.shift(@raw_metadata.size)
60
+ consistency = args.shift || @default_consistency
61
+ statement_id = connection[self]
62
+ request = Protocol::ExecuteRequest.new(statement_id, @raw_metadata, bound_args, consistency)
63
+ @request_runner.execute(connection, request)
64
+ end
23
65
  end
24
66
  end
25
67
  end