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.
@@ -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