cql-rb 1.1.0.pre3 → 1.1.0.pre6

Sign up to get free protection for your applications and to get access to all the features.
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