cql-rb 1.1.0.pre0 → 1.1.0.pre1
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/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
|