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.
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
+ # @private
4
5
  class ByteBuffer
5
6
  def initialize(initial_bytes='')
6
7
  @read_buffer = ''
@@ -46,11 +46,25 @@ module Cql
46
46
  module Client
47
47
  InvalidKeyspaceNameError = Class.new(ClientError)
48
48
 
49
- # Create a new client and connects to Cassandra.
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] :host ('localhost') One or more (comma separated)
53
- # hostnames for the Cassandra nodes you want to connect to.
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
- # If `:keyspace` was specified when the client was created the current
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/asynchronous_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
- @host = options[:host] || 'localhost'
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
- return Future.failed(NotConnectedError.new) unless can_execute?
83
- return Future.failed(InvalidKeyspaceNameError.new(%("#{keyspace}" is not a valid keyspace name))) unless valid_keyspace_name?(keyspace)
84
- connections = @lock.synchronize do
85
- @connections.select { |c| c.keyspace != keyspace }
86
- end
87
- if connections.any?
88
- futures = connections.map do |connection|
89
- execute_request(Protocol::QueryRequest.new("USE #{keyspace}", :one), connection)
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
- consistency ||= DEFAULT_CONSISTENCY_LEVEL
100
- return Future.failed(NotConnectedError.new) unless can_execute?
101
- execute_request(Protocol::QueryRequest.new(cql, consistency))
102
- rescue => e
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
- return Future.failed(NotConnectedError.new) unless can_execute?
108
- execute_request(Protocol::PrepareRequest.new(cql))
109
- rescue => e
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
- hosts_connected_future = @io_reactor.start.flat_map do
146
- hosts = @host.split(',')
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
- hosts_connected_future.on_complete do |connections|
151
- @connections = connections
193
+ f.on_failure do |e|
194
+ fail_connecting(e)
152
195
  end
153
- if @initial_keyspace
154
- initialized_future = hosts_connected_future.flat_map do |*args|
155
- use(@initial_keyspace)
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
- initialized_future = hosts_connected_future
212
+ @connected_future.fail!(e)
159
213
  end
160
- initialized_future.on_failure do |e|
161
- close
162
- if e.is_a?(Cql::QueryError) && e.code == 0x100
163
- @connected_future.fail!(AuthenticationError.new(e.message))
164
- else
165
- @connected_future.fail!(e)
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
- initialized_future.on_complete do
169
- @connected_future.complete!(self)
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
- started = execute_request(Protocol::StartupRequest.new, connection)
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
@@ -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