cql-rb 1.1.0.pre0 → 1.1.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/cql/byte_buffer.rb +1 -0
- data/lib/cql/client.rb +20 -8
- data/lib/cql/client/asynchronous_client.rb +128 -43
- data/lib/cql/client/synchronous_client.rb +25 -5
- data/lib/cql/client/synchronous_prepared_statement.rb +4 -2
- data/lib/cql/future.rb +97 -0
- data/lib/cql/io/connection.rb +1 -0
- data/lib/cql/io/io_reactor.rb +2 -0
- data/lib/cql/protocol/cql_protocol_handler.rb +16 -1
- data/lib/cql/version.rb +1 -1
- data/spec/cql/client/asynchronous_client_spec.rb +286 -52
- data/spec/cql/client/synchronous_client_spec.rb +24 -1
- data/spec/cql/client/synchronous_prepared_statement_spec.rb +24 -0
- data/spec/cql/future_spec.rb +134 -0
- data/spec/cql/protocol/cql_protocol_handler_spec.rb +7 -0
- data/spec/support/fake_io_reactor.rb +53 -13
- metadata +2 -4
- data/spec/cql/client/client_shared.rb +0 -27
data/lib/cql/byte_buffer.rb
CHANGED
data/lib/cql/client.rb
CHANGED
@@ -46,11 +46,25 @@ module Cql
|
|
46
46
|
module Client
|
47
47
|
InvalidKeyspaceNameError = Class.new(ClientError)
|
48
48
|
|
49
|
-
# Create a new client and
|
49
|
+
# Create a new client and connect to Cassandra.
|
50
50
|
#
|
51
|
+
# By default the client will connect to localhost port 9042, which can be
|
52
|
+
# overridden with the `:hosts` and `:port` options, respectively. Once
|
53
|
+
# connected to the hosts given in `:hosts` the rest of the nodes in the
|
54
|
+
# cluster will automatically be discovered and connected to.
|
55
|
+
#
|
56
|
+
# The connection will succeed if at least one node is up. Nodes that don't
|
57
|
+
# respond within the specified timeout, or where the connection initialization
|
58
|
+
# fails for some reason, are ignored.
|
59
|
+
#
|
60
|
+
# @raise Cql::Io::ConnectionError when a connection couldn't be established
|
61
|
+
# to any node
|
51
62
|
# @param [Hash] options
|
52
|
-
# @option options [String] :
|
53
|
-
# hostnames
|
63
|
+
# @option options [Array<String>] :hosts (['localhost']) One or more
|
64
|
+
# hostnames used as seed nodes when connecting. Duplicates will be removed.
|
65
|
+
# @option options [String] :host ('localhost') A comma separated list of
|
66
|
+
# hostnames to use as seed nodes. This is a backwards-compatible version
|
67
|
+
# of the :hosts option, and is deprecated.
|
54
68
|
# @option options [String] :port (9042) The port to connect to
|
55
69
|
# @option options [Integer] :connection_timeout (5) Max time to wait for a
|
56
70
|
# connection, in seconds
|
@@ -70,9 +84,7 @@ module Cql
|
|
70
84
|
# You must call this method before you call any of the other methods of a
|
71
85
|
# client. Calling it again will have no effect.
|
72
86
|
#
|
73
|
-
#
|
74
|
-
# keyspace will also be changed (otherwise the current keyspace will not
|
75
|
-
# be set).
|
87
|
+
# @see Cql::Client.connect
|
76
88
|
#
|
77
89
|
# @return [Cql::Client]
|
78
90
|
|
@@ -156,8 +168,8 @@ end
|
|
156
168
|
require 'cql/client/column_metadata'
|
157
169
|
require 'cql/client/result_metadata'
|
158
170
|
require 'cql/client/query_result'
|
171
|
+
require 'cql/client/asynchronous_client'
|
159
172
|
require 'cql/client/asynchronous_prepared_statement'
|
160
|
-
require 'cql/client/synchronous_prepared_statement'
|
161
173
|
require 'cql/client/synchronous_client'
|
162
|
-
require 'cql/client/
|
174
|
+
require 'cql/client/synchronous_prepared_statement'
|
163
175
|
require 'cql/client/request_runner'
|
@@ -6,7 +6,7 @@ module Cql
|
|
6
6
|
class AsynchronousClient < Client
|
7
7
|
def initialize(options={})
|
8
8
|
@connection_timeout = options[:connection_timeout] || 10
|
9
|
-
@
|
9
|
+
@hosts = extract_hosts(options)
|
10
10
|
@port = options[:port] || 9042
|
11
11
|
@io_reactor = options[:io_reactor] || Io::IoReactor.new(Protocol::CqlProtocolHandler)
|
12
12
|
@lock = Mutex.new
|
@@ -23,6 +23,7 @@ module Cql
|
|
23
23
|
return @connected_future if can_execute?
|
24
24
|
@connecting = true
|
25
25
|
@connected_future = Future.new
|
26
|
+
@connections = []
|
26
27
|
end
|
27
28
|
when_not_closing do
|
28
29
|
setup_connections
|
@@ -79,35 +80,30 @@ module Cql
|
|
79
80
|
end
|
80
81
|
|
81
82
|
def use(keyspace)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
83
|
+
with_failure_handler do
|
84
|
+
connections = @lock.synchronize do
|
85
|
+
@connections.select { |c| c.keyspace != keyspace }
|
86
|
+
end
|
87
|
+
if connections.any?
|
88
|
+
futures = connections.map { |connection| use_keyspace(keyspace, connection) }
|
89
|
+
Future.combine(*futures).map { nil }
|
90
|
+
else
|
91
|
+
Future.completed(nil)
|
90
92
|
end
|
91
|
-
futures.compact!
|
92
|
-
Future.combine(*futures).map { nil }
|
93
|
-
else
|
94
|
-
Future.completed(nil)
|
95
93
|
end
|
96
94
|
end
|
97
95
|
|
98
96
|
def execute(cql, consistency=nil)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
Future.failed(e)
|
97
|
+
with_failure_handler do
|
98
|
+
consistency ||= DEFAULT_CONSISTENCY_LEVEL
|
99
|
+
execute_request(Protocol::QueryRequest.new(cql, consistency))
|
100
|
+
end
|
104
101
|
end
|
105
102
|
|
106
103
|
def prepare(cql)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
Future.failed(e)
|
104
|
+
with_failure_handler do
|
105
|
+
execute_request(Protocol::PrepareRequest.new(cql))
|
106
|
+
end
|
111
107
|
end
|
112
108
|
|
113
109
|
private
|
@@ -115,6 +111,28 @@ module Cql
|
|
115
111
|
KEYSPACE_NAME_PATTERN = /^\w[\w\d_]*$/
|
116
112
|
DEFAULT_CONSISTENCY_LEVEL = :quorum
|
117
113
|
|
114
|
+
class FailedConnection
|
115
|
+
attr_reader :error
|
116
|
+
|
117
|
+
def initialize(error)
|
118
|
+
@error = error
|
119
|
+
end
|
120
|
+
|
121
|
+
def connected?
|
122
|
+
false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def extract_hosts(options)
|
127
|
+
if options[:hosts]
|
128
|
+
options[:hosts].uniq
|
129
|
+
elsif options[:host]
|
130
|
+
options[:host].split(',').uniq
|
131
|
+
else
|
132
|
+
%w[localhost]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
118
136
|
def can_execute?
|
119
137
|
@connected || @connecting
|
120
138
|
end
|
@@ -123,6 +141,13 @@ module Cql
|
|
123
141
|
name =~ KEYSPACE_NAME_PATTERN
|
124
142
|
end
|
125
143
|
|
144
|
+
def with_failure_handler
|
145
|
+
return Future.failed(NotConnectedError.new) unless can_execute?
|
146
|
+
yield
|
147
|
+
rescue => e
|
148
|
+
Future.failed(e)
|
149
|
+
end
|
150
|
+
|
126
151
|
def when_not_connecting(&callback)
|
127
152
|
if @connecting
|
128
153
|
@connected_future.on_complete(&callback)
|
@@ -141,43 +166,103 @@ module Cql
|
|
141
166
|
end
|
142
167
|
end
|
143
168
|
|
169
|
+
def discover_peers(seed_connections, initial_keyspace)
|
170
|
+
connected_seeds = seed_connections.select(&:connected?)
|
171
|
+
connection = connected_seeds.sample
|
172
|
+
return Future.completed([]) unless connection
|
173
|
+
request = Protocol::QueryRequest.new('SELECT data_center, host_id, rpc_address FROM system.peers', :one)
|
174
|
+
peer_info = execute_request(request, connection)
|
175
|
+
peer_info.flat_map do |result|
|
176
|
+
seed_dcs = connected_seeds.map { |c| c[:data_center] }.uniq
|
177
|
+
unconnected_peers = result.select do |row|
|
178
|
+
seed_dcs.include?(row['data_center']) && connected_seeds.none? { |c| c[:host_id] == row['host_id'] }
|
179
|
+
end
|
180
|
+
node_addresses = unconnected_peers.map { |row| row['rpc_address'].to_s }
|
181
|
+
if node_addresses.any?
|
182
|
+
connect_to_hosts(node_addresses, initial_keyspace, false)
|
183
|
+
else
|
184
|
+
Future.completed([])
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
144
189
|
def setup_connections
|
145
|
-
|
146
|
-
hosts
|
147
|
-
connection_futures = hosts.map { |host| connect_to_host(host) }
|
148
|
-
Future.combine(*connection_futures)
|
190
|
+
f = @io_reactor.start.flat_map do
|
191
|
+
connect_to_hosts(@hosts, @initial_keyspace, true)
|
149
192
|
end
|
150
|
-
|
151
|
-
|
193
|
+
f.on_failure do |e|
|
194
|
+
fail_connecting(e)
|
152
195
|
end
|
153
|
-
|
154
|
-
|
155
|
-
|
196
|
+
f.on_complete do |connections|
|
197
|
+
connected_connections = connections.select(&:connected?)
|
198
|
+
if connected_connections.any?
|
199
|
+
@connections = connected_connections
|
200
|
+
@connected_future.complete!(self)
|
201
|
+
else
|
202
|
+
fail_connecting(connections.first.error)
|
156
203
|
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def fail_connecting(e)
|
208
|
+
close
|
209
|
+
if e.is_a?(Cql::QueryError) && e.code == 0x100
|
210
|
+
@connected_future.fail!(AuthenticationError.new(e.message))
|
157
211
|
else
|
158
|
-
|
212
|
+
@connected_future.fail!(e)
|
159
213
|
end
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
214
|
+
end
|
215
|
+
|
216
|
+
def connect_to_hosts(hosts, initial_keyspace, peer_discovery)
|
217
|
+
connection_futures = hosts.map do |host|
|
218
|
+
connect_to_host(host, initial_keyspace).recover do |error|
|
219
|
+
FailedConnection.new(error)
|
166
220
|
end
|
167
221
|
end
|
168
|
-
|
169
|
-
|
222
|
+
hosts_connected_future = Future.combine(*connection_futures)
|
223
|
+
if peer_discovery
|
224
|
+
hosts_connected_future.flat_map do |connections|
|
225
|
+
discover_peers(connections, initial_keyspace).map do |peer_connections|
|
226
|
+
connections + peer_connections
|
227
|
+
end
|
228
|
+
end
|
229
|
+
else
|
230
|
+
hosts_connected_future
|
170
231
|
end
|
171
232
|
end
|
172
233
|
|
173
|
-
def connect_to_host(host)
|
234
|
+
def connect_to_host(host, keyspace)
|
174
235
|
connected = @io_reactor.connect(host, @port, @connection_timeout)
|
175
236
|
connected.flat_map do |connection|
|
176
|
-
|
177
|
-
started.flat_map { |response| maybe_authenticate(response, connection) }
|
237
|
+
initialize_connection(connection, keyspace)
|
178
238
|
end
|
179
239
|
end
|
180
240
|
|
241
|
+
def initialize_connection(connection, keyspace)
|
242
|
+
started = execute_request(Protocol::StartupRequest.new, connection)
|
243
|
+
authenticated = started.flat_map { |response| maybe_authenticate(response, connection) }
|
244
|
+
identified = authenticated.flat_map { identify_node(connection) }
|
245
|
+
identified.flat_map { use_keyspace(keyspace, connection) }
|
246
|
+
end
|
247
|
+
|
248
|
+
def identify_node(connection)
|
249
|
+
request = Protocol::QueryRequest.new('SELECT data_center, host_id FROM system.local', :one)
|
250
|
+
f = execute_request(request, connection)
|
251
|
+
f.on_complete do |result|
|
252
|
+
unless result.empty?
|
253
|
+
connection[:host_id] = result.first['host_id']
|
254
|
+
connection[:data_center] = result.first['data_center']
|
255
|
+
end
|
256
|
+
end
|
257
|
+
f
|
258
|
+
end
|
259
|
+
|
260
|
+
def use_keyspace(keyspace, connection)
|
261
|
+
return Future.completed(connection) unless keyspace
|
262
|
+
return Future.failed(InvalidKeyspaceNameError.new(%("#{keyspace}" is not a valid keyspace name))) unless valid_keyspace_name?(keyspace)
|
263
|
+
execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one), connection).map { connection }
|
264
|
+
end
|
265
|
+
|
181
266
|
def maybe_authenticate(response, connection)
|
182
267
|
case response
|
183
268
|
when AuthenticationRequired
|
@@ -2,19 +2,39 @@
|
|
2
2
|
|
3
3
|
module Cql
|
4
4
|
module Client
|
5
|
+
# @private
|
6
|
+
module SynchronousBacktrace
|
7
|
+
def synchronous_backtrace
|
8
|
+
yield
|
9
|
+
rescue CqlError => e
|
10
|
+
new_backtrace = caller
|
11
|
+
if new_backtrace.first.include?(SYNCHRONOUS_BACKTRACE_METHOD_NAME)
|
12
|
+
new_backtrace = new_backtrace.drop(1)
|
13
|
+
end
|
14
|
+
e.set_backtrace(new_backtrace)
|
15
|
+
raise
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
SYNCHRONOUS_BACKTRACE_METHOD_NAME = 'synchronous_backtrace'
|
21
|
+
end
|
22
|
+
|
5
23
|
# @private
|
6
24
|
class SynchronousClient < Client
|
25
|
+
include SynchronousBacktrace
|
26
|
+
|
7
27
|
def initialize(async_client)
|
8
28
|
@async_client = async_client
|
9
29
|
end
|
10
30
|
|
11
31
|
def connect
|
12
|
-
@async_client.connect.get
|
32
|
+
synchronous_backtrace { @async_client.connect.get }
|
13
33
|
self
|
14
34
|
end
|
15
35
|
|
16
36
|
def close
|
17
|
-
@async_client.close.get
|
37
|
+
synchronous_backtrace { @async_client.close.get }
|
18
38
|
self
|
19
39
|
end
|
20
40
|
|
@@ -27,15 +47,15 @@ module Cql
|
|
27
47
|
end
|
28
48
|
|
29
49
|
def use(keyspace)
|
30
|
-
@async_client.use(keyspace).get
|
50
|
+
synchronous_backtrace { @async_client.use(keyspace).get }
|
31
51
|
end
|
32
52
|
|
33
53
|
def execute(cql, consistency=nil)
|
34
|
-
@async_client.execute(cql, consistency).get
|
54
|
+
synchronous_backtrace { @async_client.execute(cql, consistency).get }
|
35
55
|
end
|
36
56
|
|
37
57
|
def prepare(cql)
|
38
|
-
async_statement = @async_client.prepare(cql).get
|
58
|
+
async_statement = synchronous_backtrace { @async_client.prepare(cql).get }
|
39
59
|
SynchronousPreparedStatement.new(async_statement)
|
40
60
|
end
|
41
61
|
|
@@ -4,19 +4,21 @@ module Cql
|
|
4
4
|
module Client
|
5
5
|
# @private
|
6
6
|
class SynchronousPreparedStatement < PreparedStatement
|
7
|
+
include SynchronousBacktrace
|
8
|
+
|
7
9
|
def initialize(async_statement)
|
8
10
|
@async_statement = async_statement
|
9
11
|
@metadata = async_statement.metadata
|
10
12
|
end
|
11
13
|
|
12
14
|
def execute(*args)
|
13
|
-
@async_statement.execute(*args).get
|
15
|
+
synchronous_backtrace { @async_statement.execute(*args).get }
|
14
16
|
end
|
15
17
|
|
16
18
|
def pipeline
|
17
19
|
pl = Pipeline.new(@async_statement)
|
18
20
|
yield pl
|
19
|
-
pl.get
|
21
|
+
synchronous_backtrace { pl.get }
|
20
22
|
end
|
21
23
|
|
22
24
|
def async
|
data/lib/cql/future.rb
CHANGED
@@ -8,6 +8,8 @@ module Cql
|
|
8
8
|
|
9
9
|
# A future represents the value of a process that may not yet have completed.
|
10
10
|
#
|
11
|
+
# @private
|
12
|
+
#
|
11
13
|
class Future
|
12
14
|
def initialize
|
13
15
|
@complete_listeners = []
|
@@ -33,6 +35,26 @@ module Cql
|
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
38
|
+
# Returns a future which will complete with the value of the first
|
39
|
+
# (successful) of the specified futures. If all of the futures fail, the
|
40
|
+
# returned future will also fail (with the error of the last failed future).
|
41
|
+
#
|
42
|
+
# @param [Array<Future>] futures the futures to monitor
|
43
|
+
# @return [Future] a future which represents the first completing future
|
44
|
+
#
|
45
|
+
def self.first(*futures)
|
46
|
+
ff = Future.new
|
47
|
+
futures.each do |f|
|
48
|
+
f.on_complete do |value|
|
49
|
+
ff.complete!(value) unless ff.complete?
|
50
|
+
end
|
51
|
+
f.on_failure do |e|
|
52
|
+
ff.fail!(e) if futures.all?(&:failed?)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
ff
|
56
|
+
end
|
57
|
+
|
36
58
|
# Creates a new future which is completed.
|
37
59
|
#
|
38
60
|
# @param [Object, nil] value the value of the created future
|
@@ -197,6 +219,81 @@ module Cql
|
|
197
219
|
end
|
198
220
|
fp
|
199
221
|
end
|
222
|
+
|
223
|
+
# Returns a new future which represents either the value of the original
|
224
|
+
# future, or the result of the given block, if the original future fails.
|
225
|
+
#
|
226
|
+
# This method is similar to{#map}, but is triggered by a failure. You can
|
227
|
+
# also think of it as a `rescue` block for asynchronous operations.
|
228
|
+
#
|
229
|
+
# If the block raises an error a failed future with that error will be
|
230
|
+
# returned (this can be used to transform an error into another error,
|
231
|
+
# instead of tranforming an error into a value).
|
232
|
+
#
|
233
|
+
# @example
|
234
|
+
# future2 = future1.recover { |error| 'foo' }
|
235
|
+
# future1.fail!(error)
|
236
|
+
# future2.get # => 'foo'
|
237
|
+
#
|
238
|
+
# @yieldparam [Object] error the error from the original future
|
239
|
+
# @yieldreturn [Object] the value of the new future
|
240
|
+
# @return [Future] a new future representing a value recovered from the error
|
241
|
+
#
|
242
|
+
def recover(&block)
|
243
|
+
fp = Future.new
|
244
|
+
on_failure do |e|
|
245
|
+
begin
|
246
|
+
vv = block.call(e)
|
247
|
+
fp.complete!(vv)
|
248
|
+
rescue => e
|
249
|
+
fp.fail!(e)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
on_complete do |v|
|
253
|
+
fp.complete!(v)
|
254
|
+
end
|
255
|
+
fp
|
256
|
+
end
|
257
|
+
|
258
|
+
# Returns a new future which represents either the value of the original
|
259
|
+
# future, or the value of the future returned by the given block.
|
260
|
+
#
|
261
|
+
# This is like {#recover} but for cases when the handling of an error is
|
262
|
+
# itself asynchronous.
|
263
|
+
#
|
264
|
+
# If the block raises an error a failed future with that error will be
|
265
|
+
# returned (this can be used to transform an error into another error,
|
266
|
+
# instead of tranforming an error into a value).
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# future2 = future1.fallback { |error| perform_async_operation }
|
270
|
+
# future1.fail!(error)
|
271
|
+
# future2.get # => whatever the async operation resolved to
|
272
|
+
#
|
273
|
+
# @yieldparam [Object] error the error from the original future
|
274
|
+
# @yieldreturn [Object] the value of the new future
|
275
|
+
# @return [Future] a new future representing a value recovered from the error
|
276
|
+
#
|
277
|
+
def fallback(&block)
|
278
|
+
fp = Future.new
|
279
|
+
on_failure do |e|
|
280
|
+
begin
|
281
|
+
fpp = block.call(e)
|
282
|
+
fpp.on_failure do |e|
|
283
|
+
fp.fail!(e)
|
284
|
+
end
|
285
|
+
fpp.on_complete do |vv|
|
286
|
+
fp.complete!(vv)
|
287
|
+
end
|
288
|
+
rescue => e
|
289
|
+
fp.fail!(e)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
on_complete do |v|
|
293
|
+
fp.complete!(v)
|
294
|
+
end
|
295
|
+
fp
|
296
|
+
end
|
200
297
|
end
|
201
298
|
|
202
299
|
# @private
|