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.
- data/README.md +2 -2
- data/lib/cql/client.rb +9 -5
- data/lib/cql/client/asynchronous_client.rb +105 -192
- data/lib/cql/client/asynchronous_prepared_statement.rb +51 -9
- data/lib/cql/client/connection_helper.rb +155 -0
- data/lib/cql/client/connection_manager.rb +56 -0
- data/lib/cql/client/keyspace_changer.rb +27 -0
- data/lib/cql/client/null_logger.rb +21 -0
- data/lib/cql/client/request_runner.rb +5 -3
- data/lib/cql/client/synchronous_client.rb +5 -5
- data/lib/cql/client/synchronous_prepared_statement.rb +4 -8
- data/lib/cql/future.rb +320 -210
- data/lib/cql/io/connection.rb +5 -5
- data/lib/cql/io/io_reactor.rb +21 -23
- data/lib/cql/protocol/cql_protocol_handler.rb +69 -38
- data/lib/cql/protocol/encoding.rb +5 -1
- data/lib/cql/protocol/requests/register_request.rb +2 -0
- data/lib/cql/protocol/type_converter.rb +1 -0
- data/lib/cql/version.rb +1 -1
- data/spec/cql/client/asynchronous_client_spec.rb +368 -175
- data/spec/cql/client/asynchronous_prepared_statement_spec.rb +132 -22
- data/spec/cql/client/connection_helper_spec.rb +335 -0
- data/spec/cql/client/connection_manager_spec.rb +118 -0
- data/spec/cql/client/keyspace_changer_spec.rb +50 -0
- data/spec/cql/client/request_runner_spec.rb +12 -12
- data/spec/cql/client/synchronous_client_spec.rb +15 -15
- data/spec/cql/client/synchronous_prepared_statement_spec.rb +15 -11
- data/spec/cql/future_spec.rb +529 -301
- data/spec/cql/io/connection_spec.rb +12 -12
- data/spec/cql/io/io_reactor_spec.rb +61 -61
- data/spec/cql/protocol/cql_protocol_handler_spec.rb +26 -12
- data/spec/cql/protocol/encoding_spec.rb +5 -0
- data/spec/cql/protocol/type_converter_spec.rb +1 -1
- data/spec/cql/time_uuid_spec.rb +7 -7
- data/spec/integration/client_spec.rb +2 -2
- data/spec/integration/io_spec.rb +20 -20
- data/spec/integration/protocol_spec.rb +17 -17
- data/spec/integration/regression_spec.rb +6 -0
- data/spec/integration/uuid_spec.rb +4 -0
- data/spec/support/fake_io_reactor.rb +38 -8
- data/spec/support/fake_server.rb +3 -3
- 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
|
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
|
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
|
|
data/lib/cql/client.rb
CHANGED
@@ -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
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
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
|
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
|
-
@
|
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 =
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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 =
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
@
|
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 = @
|
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.
|
82
|
+
futures = connections.map { |connection| @keyspace_changer.use_keyspace(keyspace, connection) }
|
83
|
+
Future.all(*futures).map { nil }
|
90
84
|
else
|
91
|
-
Future.
|
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
|
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
|
-
|
98
|
+
AsynchronousPreparedStatement.prepare(cql, @default_consistency, @connection_manager, @logger)
|
106
99
|
end
|
107
100
|
end
|
108
101
|
|
109
102
|
private
|
110
103
|
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
190
|
-
|
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
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
211
|
-
|
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
|
243
|
-
|
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
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
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 || @
|
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
|
-
|
8
|
-
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|