cassandra-driver 1.0.0.beta.3 → 1.0.0.rc.1

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.
Files changed (53) hide show
  1. checksums.yaml +13 -5
  2. data/README.md +8 -6
  3. data/lib/cassandra.rb +99 -13
  4. data/lib/cassandra/address_resolution.rb +36 -0
  5. data/lib/cassandra/address_resolution/policies.rb +2 -0
  6. data/lib/cassandra/address_resolution/policies/ec2_multi_region.rb +56 -0
  7. data/lib/cassandra/address_resolution/policies/none.rb +35 -0
  8. data/lib/cassandra/auth.rb +1 -1
  9. data/lib/cassandra/auth/providers/password.rb +1 -1
  10. data/lib/cassandra/client.rb +2 -2
  11. data/lib/cassandra/client/batch.rb +3 -3
  12. data/lib/cassandra/client/client.rb +12 -12
  13. data/lib/cassandra/client/connection_manager.rb +2 -2
  14. data/lib/cassandra/client/connector.rb +6 -10
  15. data/lib/cassandra/client/prepared_statement.rb +4 -4
  16. data/lib/cassandra/client/request_runner.rb +1 -2
  17. data/lib/cassandra/cluster.rb +15 -5
  18. data/lib/cassandra/cluster/client.rb +158 -96
  19. data/lib/cassandra/cluster/connector.rb +42 -27
  20. data/lib/cassandra/cluster/control_connection.rb +384 -132
  21. data/lib/cassandra/cluster/options.rb +5 -2
  22. data/lib/cassandra/cluster/registry.rb +19 -9
  23. data/lib/cassandra/compression.rb +1 -1
  24. data/lib/cassandra/compression/compressors/lz4.rb +1 -1
  25. data/lib/cassandra/compression/compressors/snappy.rb +1 -1
  26. data/lib/cassandra/driver.rb +28 -20
  27. data/lib/cassandra/errors.rb +325 -35
  28. data/lib/cassandra/future.rb +3 -3
  29. data/lib/cassandra/load_balancing/policies/dc_aware_round_robin.rb +7 -3
  30. data/lib/cassandra/load_balancing/policies/token_aware.rb +1 -1
  31. data/lib/cassandra/protocol.rb +0 -16
  32. data/lib/cassandra/protocol/cql_byte_buffer.rb +18 -18
  33. data/lib/cassandra/protocol/cql_protocol_handler.rb +74 -8
  34. data/lib/cassandra/protocol/frame_decoder.rb +2 -2
  35. data/lib/cassandra/protocol/frame_encoder.rb +1 -1
  36. data/lib/cassandra/protocol/response.rb +1 -1
  37. data/lib/cassandra/protocol/responses/detailed_error_response.rb +16 -1
  38. data/lib/cassandra/protocol/responses/error_response.rb +17 -0
  39. data/lib/cassandra/protocol/responses/event_response.rb +1 -1
  40. data/lib/cassandra/protocol/responses/raw_rows_result_response.rb +1 -1
  41. data/lib/cassandra/protocol/responses/result_response.rb +1 -1
  42. data/lib/cassandra/protocol/responses/rows_result_response.rb +1 -1
  43. data/lib/cassandra/protocol/type_converter.rb +4 -3
  44. data/lib/cassandra/reconnection.rb +1 -1
  45. data/lib/cassandra/retry.rb +3 -5
  46. data/lib/cassandra/session.rb +11 -5
  47. data/lib/cassandra/table.rb +1 -1
  48. data/lib/cassandra/time_uuid.rb +21 -83
  49. data/lib/cassandra/util.rb +1 -1
  50. data/lib/cassandra/uuid.rb +6 -4
  51. data/lib/cassandra/uuid/generator.rb +207 -0
  52. data/lib/cassandra/version.rb +1 -1
  53. metadata +24 -19
@@ -43,25 +43,17 @@ module Cassandra
43
43
  end
44
44
  end
45
45
 
46
- @logger.info("Connecting ip=#{host.ip}")
47
-
48
46
  f = do_connect(host)
49
47
 
50
48
  f.on_failure do |error|
51
- @logger.warn("Connection failed ip=#{host.ip} error=\"#{error.class.name}: #{error.message}\"")
52
49
  connection_error(host, error)
53
50
  end
54
51
 
55
52
  f.on_value do |connection|
56
53
  connection.on_closed do |cause|
57
- message = "Disconnected ip=#{host.ip}"
58
- message << " error=#{cause.message}" if cause
59
-
60
- @logger.info(message)
61
54
  disconnected(host, cause)
62
55
  end
63
56
 
64
- @logger.info("Connected ip=#{host.ip}")
65
57
  connected(host)
66
58
  end
67
59
 
@@ -75,22 +67,15 @@ module Cassandra
75
67
  return Future.resolved
76
68
  end
77
69
 
78
- @logger.info("Refreshing host status ip=#{host.ip}")
70
+ @logger.debug("Checking if host #{host.ip} is up")
79
71
  f = do_connect(host)
80
72
 
81
73
  f.on_failure do |error|
82
- @logger.info("Refreshed host status ip=#{host.ip}")
83
- @logger.warn("Connection failed ip=#{host.ip} error=\"#{error.class.name}: #{error.message}\"")
84
74
  connection_error(host, error)
85
75
  end
86
76
 
87
77
  f.on_value do |connection|
88
- @logger.info("Refreshed host status ip=#{host.ip}")
89
78
  connection.on_closed do |cause|
90
- message = "Disconnected ip=#{host.ip}"
91
- message << " error=#{cause.message}" if cause
92
-
93
- @logger.info(message)
94
79
  disconnected(host, cause)
95
80
  end
96
81
 
@@ -99,7 +84,21 @@ module Cassandra
99
84
  @open_connections[host] << connection
100
85
  end
101
86
 
102
- @logger.info("Connected ip=#{host.ip}")
87
+ timer = @reactor.schedule_timer(UNCLAIMED_TIMEOUT)
88
+ timer.on_value do
89
+ close = false
90
+
91
+ synchronize do
92
+ open_connections = @open_connections[host]
93
+ if open_connections
94
+ close = !open_connections.delete(connection).nil?
95
+ @open_connections.delete(host) if open_connections.empty?
96
+ end
97
+ end
98
+
99
+ connection.close if close
100
+ end
101
+
103
102
  connected(host)
104
103
  end
105
104
 
@@ -112,22 +111,28 @@ module Cassandra
112
111
 
113
112
  private
114
113
 
115
- NO_CONNECTIONS = Ione::Future.resolved([])
114
+ NO_CONNECTIONS = Ione::Future.resolved([])
115
+ UNCLAIMED_TIMEOUT = 5 # close unclaimed connections in five seconds
116
116
 
117
117
  def do_connect(host)
118
118
  create_connector.connect(host.ip.to_s).fallback do |error|
119
- if error.is_a?(Errors::QueryError) && error.code == 0x0a
119
+ case error
120
+ when Errors::ProtocolError
120
121
  synchronize do
121
122
  if @options.protocol_version > 1
122
- @logger.warn('Could not connect using protocol version %d (will try again with %d): %s' % [@options.protocol_version, @options.protocol_version - 1, error.message])
123
+ @logger.info("Host #{host.ip} doesn't support protocol version #{@options.protocol_version}, downgrading")
123
124
  @options.protocol_version -= 1
124
125
  do_connect(host)
125
126
  else
126
127
  Ione::Future.failed(error)
127
128
  end
128
129
  end
129
- else
130
+ when Error
130
131
  Ione::Future.failed(error)
132
+ else
133
+ e = Errors::IOError.new(error.message)
134
+ e.set_backtrace(error.backtrace)
135
+ Ione::Future.failed(e)
131
136
  end
132
137
  end
133
138
  end
@@ -135,8 +140,8 @@ module Cassandra
135
140
  def create_connector
136
141
  authentication_step = @options.protocol_version == 1 ? Cassandra::Client::CredentialsAuthenticationStep.new(@options.credentials) : Cassandra::Client::SaslAuthenticationStep.new(@options.auth_provider)
137
142
  protocol_handler_factory = lambda do |connection|
138
- raise "no connection given" unless connection
139
- Protocol::CqlProtocolHandler.new(connection, @reactor, @options.protocol_version, @options.compressor)
143
+ raise Errors::ClientError, 'Not connected, reactor stopped' unless connection
144
+ Protocol::CqlProtocolHandler.new(connection, @reactor, @options.protocol_version, @options.compressor, @options.heartbeat_interval, @options.idle_timeout)
140
145
  end
141
146
 
142
147
  Cassandra::Client::Connector.new([
@@ -216,14 +221,19 @@ module Cassandra
216
221
  connections -= 1
217
222
 
218
223
  if connections == 0
219
- notify = !error.nil? && !error.is_a?(Cassandra::Error)
224
+ notify = !error.nil?
220
225
  @connections.delete(host)
221
226
  else
222
227
  @connections[host] = connections
223
228
  end
224
229
  end
225
230
 
226
- @registry.host_down(host.ip) if notify
231
+ @logger.debug("Host #{host.ip} closed connection (#{error.class.name}: #{error.message})") if error
232
+
233
+ if notify
234
+ @logger.warn("Host #{host.ip} closed all connections")
235
+ @registry.host_down(host.ip)
236
+ end
227
237
 
228
238
  self
229
239
  end
@@ -232,10 +242,15 @@ module Cassandra
232
242
  notify = false
233
243
 
234
244
  synchronize do
235
- notify = !error.is_a?(Cassandra::Error) && !@connections.has_key?(host)
245
+ notify = !error.nil? && !@connections.has_key?(host)
236
246
  end
237
247
 
238
- @registry.host_down(host.ip) if notify
248
+ @logger.debug("Host #{host.ip} refused connection (#{error.class.name}: #{error.message})")
249
+
250
+ if notify
251
+ @logger.warn("Host #{host.ip} refused all connections")
252
+ @registry.host_down(host.ip)
253
+ end
239
254
 
240
255
  self
241
256
  end
@@ -22,7 +22,7 @@ module Cassandra
22
22
  class ControlConnection
23
23
  include MonitorMixin
24
24
 
25
- def initialize(logger, io_reactor, cluster_registry, cluster_schema, cluster_metadata, load_balancing_policy, reconnection_policy, connector)
25
+ def initialize(logger, io_reactor, cluster_registry, cluster_schema, cluster_metadata, load_balancing_policy, reconnection_policy, address_resolution_policy, connector)
26
26
  @logger = logger
27
27
  @io_reactor = io_reactor
28
28
  @registry = cluster_registry
@@ -30,9 +30,15 @@ module Cassandra
30
30
  @metadata = cluster_metadata
31
31
  @load_balancing_policy = load_balancing_policy
32
32
  @reconnection_policy = reconnection_policy
33
+ @address_resolver = address_resolution_policy
33
34
  @connector = connector
34
35
  @refreshing_statuses = Hash.new(false)
35
36
  @status = :closed
37
+ @refreshing_schema = false
38
+ @refreshing_keyspaces = Hash.new(false)
39
+ @refreshing_tables = Hash.new
40
+ @refreshing_hosts = false
41
+ @refreshing_host = Hash.new(false)
36
42
 
37
43
  mon_initialize
38
44
  end
@@ -43,12 +49,14 @@ module Cassandra
43
49
  @status = :connecting
44
50
  end
45
51
 
46
- @logger.debug('Establishing control connection')
47
-
48
- @io_reactor.start.flat_map do
52
+ f = @io_reactor.start.flat_map do
49
53
  plan = @load_balancing_policy.plan(nil, VOID_STATEMENT, VOID_OPTIONS)
50
54
  connect_to_first_available(plan)
51
55
  end
56
+ f.on_failure do |e|
57
+ close_async
58
+ end
59
+ f
52
60
  end
53
61
 
54
62
  def host_found(host)
@@ -56,31 +64,48 @@ module Cassandra
56
64
 
57
65
  def host_lost(host)
58
66
  synchronize do
59
- @refreshing_statuses.delete(host)
67
+ timer = @refreshing_statuses.delete(host)
68
+ @io_reactor.cancel_timer(timer) if timer
60
69
  end
70
+
71
+ nil
61
72
  end
62
73
 
63
74
  def host_up(host)
64
75
  synchronize do
65
- @refreshing_statuses.delete(host)
76
+ timer = @refreshing_statuses.delete(host)
77
+ @io_reactor.cancel_timer(timer) if timer
66
78
 
67
- return connect_async if !@connection && !(@status == :closed || @status == :closed)
79
+ unless @connection || (@status == :closing || @status == :closed) || @load_balancing_policy.distance(host) == :ignore
80
+ return connect_to_first_available(@load_balancing_policy.plan(nil, VOID_STATEMENT, VOID_OPTIONS))
81
+ end
68
82
  end
69
83
 
70
84
  Ione::Future.resolved
71
85
  end
72
86
 
73
87
  def host_down(host)
88
+ schedule = nil
89
+ timer = nil
90
+
74
91
  synchronize do
75
- return Ione::Future.resolved if @refreshing_statuses[host]
92
+ return Ione::Future.resolved if @refreshing_statuses[host] || @load_balancing_policy.distance(host) == :ignore
93
+
94
+ schedule = @reconnection_policy.schedule
95
+ timeout = schedule.next
76
96
 
77
- @logger.debug("Starting to continuously refresh status for ip=#{host.ip}")
78
- @refreshing_statuses[host] = true
97
+ @logger.debug("Starting to continuously refresh status of #{host.ip} in #{timeout} seconds")
98
+
99
+ @refreshing_statuses[host] = timer = @io_reactor.schedule_timer(timeout)
79
100
  end
80
101
 
81
- refresh_host_status(host).fallback do |e|
82
- refresh_host_status_with_retry(host, @reconnection_policy.schedule)
102
+ timer.on_value do
103
+ refresh_host_status(host).fallback do |e|
104
+ refresh_host_status_with_retry(timer, host, schedule)
105
+ end
83
106
  end
107
+
108
+ nil
84
109
  end
85
110
 
86
111
  def close_async
@@ -116,19 +141,18 @@ module Cassandra
116
141
  f = @io_reactor.schedule_timer(timeout)
117
142
  f = f.flat_map do
118
143
  if synchronize { @status == :reconnecting }
119
- @logger.debug('Reestablishing control connection')
120
144
  plan = @load_balancing_policy.plan(nil, VOID_STATEMENT, VOID_OPTIONS)
121
145
  connect_to_first_available(plan)
122
146
  else
123
- @logger.debug('Stopping reconnection')
124
147
  Ione::Future.resolved
125
148
  end
126
149
  end
127
- f.fallback do
150
+ f.fallback do |e|
151
+ @logger.error("Control connection failed (#{e.class.name}: #{e.message})")
152
+
128
153
  if synchronize { @status == :reconnecting }
129
154
  reconnect_async(schedule)
130
155
  else
131
- @logger.debug('Stopping reconnection')
132
156
  return Ione::Future.resolved
133
157
  end
134
158
  end
@@ -137,13 +161,20 @@ module Cassandra
137
161
  def register_async
138
162
  connection = @connection
139
163
 
140
- return Ione::Future.failed("not connected") if connection.nil?
141
-
142
- @logger.debug('Registering for events')
143
-
144
- connection.send_request(REGISTER).map do
145
- @logger.debug('Registered for events')
164
+ return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil?
146
165
 
166
+ f = connection.send_request(REGISTER)
167
+ f = f.map do |r|
168
+ case r
169
+ when Protocol::ReadyResponse
170
+ nil
171
+ when Protocol::ErrorResponse
172
+ raise r.to_error(VOID_STATEMENT)
173
+ else
174
+ raise Errors::InternalError, "Unexpected response #{r.inspect}"
175
+ end
176
+ end
177
+ f = f.map do
147
178
  connection.on_event do |event|
148
179
  @logger.debug("Event received #{event}")
149
180
 
@@ -151,21 +182,21 @@ module Cassandra
151
182
  case event.change
152
183
  when 'CREATED'
153
184
  if event.table.empty?
154
- refresh_schema_async
185
+ refresh_schema_async_maybe_retry
155
186
  else
156
- refresh_keyspace_async(event.keyspace)
187
+ refresh_keyspace_async_maybe_retry(event.keyspace)
157
188
  end
158
189
  when 'DROPPED'
159
190
  if event.table.empty?
160
- refresh_schema_async
191
+ refresh_schema_async_maybe_retry
161
192
  else
162
- refresh_keyspace_async(event.keyspace)
193
+ refresh_keyspace_async_maybe_retry(event.keyspace)
163
194
  end
164
195
  when 'UPDATED'
165
196
  if event.table.empty?
166
- refresh_keyspace_async(event.keyspace)
197
+ refresh_keyspace_async_maybe_retry(event.keyspace)
167
198
  else
168
- refresh_table_async(event.keyspace, event.table)
199
+ refresh_table_async_maybe_retry(event.keyspace, event.table)
169
200
  end
170
201
  end
171
202
  else
@@ -173,19 +204,19 @@ module Cassandra
173
204
  when 'UP'
174
205
  address = event.address
175
206
 
176
- refresh_host_async(address) if @registry.has_host?(address)
207
+ refresh_host_async_maybe_retry(address) if @registry.has_host?(address)
177
208
  when 'DOWN'
178
209
  @registry.host_down(event.address)
179
210
  when 'NEW_NODE'
180
211
  address = event.address
181
212
 
182
213
  unless @registry.has_host?(address)
183
- refresh_host_async(address)
184
- refresh_schema_async
214
+ refresh_host_async_maybe_retry(address)
215
+ refresh_schema_async_maybe_retry
185
216
  end
186
217
  when 'REMOVED_NODE'
187
218
  @registry.host_lost(event.address)
188
- refresh_schema_async
219
+ refresh_schema_async_maybe_retry
189
220
  end
190
221
  end
191
222
  end
@@ -197,85 +228,247 @@ module Cassandra
197
228
  def refresh_schema_async
198
229
  connection = @connection
199
230
 
200
- return Ione::Future.failed("not connected") if connection.nil?
201
-
202
- @logger.debug('Fetching schema metadata')
231
+ return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil?
203
232
 
204
- keyspaces = connection.send_request(SELECT_KEYSPACES)
205
- tables = connection.send_request(SELECT_TABLES)
206
- columns = connection.send_request(SELECT_COLUMNS)
233
+ keyspaces = send_select_request(connection, SELECT_KEYSPACES)
234
+ tables = send_select_request(connection, SELECT_TABLES)
235
+ columns = send_select_request(connection, SELECT_COLUMNS)
207
236
 
208
237
  Ione::Future.all(keyspaces, tables, columns).map do |(keyspaces, tables, columns)|
209
- @logger.debug('Fetched schema metadata')
210
-
211
238
  host = @registry.host(connection.host)
212
239
 
213
- @schema.update_keyspaces(host, keyspaces.rows, tables.rows, columns.rows)
240
+ @schema.update_keyspaces(host, keyspaces, tables, columns)
214
241
  @metadata.rebuild_token_map
215
242
  end
216
243
  end
217
244
 
245
+ def refresh_schema_async_maybe_retry
246
+ synchronize do
247
+ return Ione::Future.resolved if @refreshing_schema
248
+ @refreshing_schema = true
249
+ end
250
+
251
+ refresh_schema_async.fallback do |e|
252
+ case e
253
+ when Errors::HostError
254
+ refresh_schema_async_retry(e, @reconnection_policy.schedule)
255
+ else
256
+ connection = @connection
257
+ connection && connection.close(e)
258
+
259
+ Ione::Future.resolved
260
+ end
261
+ end.map do
262
+ synchronize do
263
+ @refreshing_schema = false
264
+ end
265
+ end
266
+ end
267
+
268
+ def refresh_schema_async_retry(error, schedule)
269
+ timeout = schedule.next
270
+ @logger.info("Failed to refresh schema (#{error.class.name}: #{error.message}), retrying in #{timeout}")
271
+
272
+ timer = @io_reactor.schedule_timer(timeout)
273
+ timer.flat_map do
274
+ refresh_schema_async.fallback do |e|
275
+ case e
276
+ when Errors::HostError
277
+ refresh_schema_async_retry(e, schedule)
278
+ else
279
+ connection = @connection
280
+ connection && connection.close(e)
281
+
282
+ Ione::Future.resolved
283
+ end
284
+ end
285
+ end
286
+ end
287
+
288
+ def refresh_keyspace_async_maybe_retry(keyspace)
289
+ synchronize do
290
+ return Ione::Future.resolved if @refreshing_schema || @refreshing_keyspaces[keyspace]
291
+ @refreshing_keyspaces[keyspace] = true
292
+ end
293
+
294
+ refresh_keyspace_async(keyspace).fallback do |e|
295
+ case e
296
+ when Errors::HostError
297
+ refresh_keyspace_async_retry(keyspace, e, @reconnection_policy.schedule)
298
+ else
299
+ connection = @connection
300
+ connection && connection.close(e)
301
+
302
+ Ione::Future.resolved
303
+ end
304
+ end.map do
305
+ synchronize do
306
+ @refreshing_keyspaces.delete(host)
307
+ end
308
+ end
309
+ end
310
+
311
+ def refresh_keyspace_async_retry(keyspace, error, schedule)
312
+ timeout = schedule.next
313
+ @logger.info("Failed to refresh keyspace #{keyspace} (#{error.class.name}: #{error.message}), retrying in #{timeout}")
314
+
315
+ timer = @io_reactor.schedule_timer(timeout)
316
+ timer.flat_map do
317
+ refresh_keyspace_async(keyspace).fallback do |e|
318
+ case e
319
+ when Errors::HostError
320
+ refresh_keyspace_async_retry(keyspace, e, schedule)
321
+ else
322
+ connection = @connection
323
+ connection && connection.close(e)
324
+
325
+ Ione::Future.resolved
326
+ end
327
+ end
328
+ end
329
+ end
330
+
218
331
  def refresh_keyspace_async(keyspace)
219
332
  connection = @connection
220
333
 
221
- return Ione::Future.failed("not connected") if connection.nil?
222
-
223
- @logger.debug("Fetching keyspace #{keyspace.inspect} metadata")
334
+ return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil?
224
335
 
225
336
  params = [keyspace]
226
- keyspaces = connection.send_request(Protocol::QueryRequest.new("SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?", params, nil, :one))
227
- tables = connection.send_request(Protocol::QueryRequest.new("SELECT * FROM system.schema_columnfamilies WHERE keyspace_name = ?", params, nil, :one))
228
- columns = connection.send_request(Protocol::QueryRequest.new("SELECT * FROM system.schema_columns WHERE keyspace_name = ?", params, nil, :one))
337
+ keyspaces = send_select_request(connection, Protocol::QueryRequest.new("SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?", params, nil, :one))
338
+ tables = send_select_request(connection, Protocol::QueryRequest.new("SELECT * FROM system.schema_columnfamilies WHERE keyspace_name = ?", params, nil, :one))
339
+ columns = send_select_request(connection, Protocol::QueryRequest.new("SELECT * FROM system.schema_columns WHERE keyspace_name = ?", params, nil, :one))
229
340
 
230
341
  Ione::Future.all(keyspaces, tables, columns).map do |(keyspaces, tables, columns)|
231
- @logger.debug("Fetched keyspace #{keyspace.inspect} metadata")
232
-
233
342
  host = @registry.host(connection.host)
234
343
 
235
- @schema.update_keyspace(host, keyspaces.rows.first, tables.rows, columns.rows)
344
+ @schema.update_keyspace(host, keyspaces.first, tables, columns) unless keyspaces.empty?
345
+ end
346
+ end
347
+
348
+ def refresh_table_async_maybe_retry(keyspace, table)
349
+ synchronize do
350
+ return Ione::Future.resolved if @refreshing_schema || @refreshing_keyspaces[keyspace] || @refreshing_tables[keyspace][table]
351
+ @refreshing_tables[keyspace] ||= ::Hash.new(false)
352
+ @refreshing_tables[keyspace][table] = true
353
+ end
354
+
355
+ refresh_table_async(keyspace, table).fallback do |e|
356
+ case e
357
+ when Errors::HostError
358
+ refresh_keyspace_async_retry(keyspace, e, @reconnection_policy.schedule)
359
+ else
360
+ connection = @connection
361
+ connection && connection.close(e)
362
+
363
+ Ione::Future.resolved
364
+ end
365
+ end.map do
366
+ synchronize do
367
+ @refreshing_tables[keyspace].delete(table)
368
+ @refreshing_tables.delete(keyspace) if @refreshing_tables[keyspace].empty?
369
+ end
370
+ end
371
+ end
372
+
373
+ def refresh_table_async_retry(keyspace, table, error, schedule)
374
+ timeout = schedule.next
375
+ @logger.info("Failed to refresh keyspace #{keyspace} (#{error.class.name}: #{error.message}), retrying in #{timeout}")
376
+
377
+ timer = @io_reactor.schedule_timer(timeout)
378
+ timer.flat_map do
379
+ refresh_keyspace_async(keyspace).fallback do |e|
380
+ case e
381
+ when Errors::HostError
382
+ refresh_keyspace_async_retry(keyspace, e, schedule)
383
+ else
384
+ connection = @connection
385
+ connection && connection.close(e)
386
+
387
+ Ione::Future.resolved
388
+ end
389
+ end
236
390
  end
237
391
  end
238
392
 
239
393
  def refresh_table_async(keyspace, table)
240
394
  connection = @connection
241
395
 
242
- return Ione::Future.failed("not connected") if connection.nil?
243
-
244
- @logger.debug("Fetching table \"#{keyspace}.#{table}\" metadata")
396
+ return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil?
245
397
 
246
398
  params = [keyspace, table]
247
- table = connection.send_request(Protocol::QueryRequest.new("SELECT * FROM system.schema_columnfamilies WHERE keyspace_name = ? AND columnfamily_name = ?", params, nil, :one))
248
- columns = connection.send_request(Protocol::QueryRequest.new("SELECT * FROM system.schema_columns WHERE keyspace_name = ? AND columnfamily_name = ?", params, nil, :one))
399
+ table = send_select_request(connection, Protocol::QueryRequest.new("SELECT * FROM system.schema_columnfamilies WHERE keyspace_name = ? AND columnfamily_name = ?", params, nil, :one))
400
+ columns = send_select_request(connection, Protocol::QueryRequest.new("SELECT * FROM system.schema_columns WHERE keyspace_name = ? AND columnfamily_name = ?", params, nil, :one))
249
401
 
250
402
  Ione::Future.all(table, columns).map do |(table, columns)|
251
- @logger.debug("Fetched table \"#{keyspace}.#{table}\" metadata")
252
-
253
403
  host = @registry.host(connection.host)
254
404
 
255
- @schema.udpate_table(host, keyspace, table.rows, columns.rows)
405
+ @schema.udpate_table(host, keyspace, table, columns)
256
406
  end
257
407
  end
258
408
 
259
- def refresh_hosts_async
260
- connection = @connection
409
+ def refresh_hosts_async_maybe_retry
410
+ synchronize do
411
+ return Ione::Future.resolved if @refreshing_hosts
412
+ @refreshing_hosts = true
413
+ end
261
414
 
262
- return Ione::Future.failed("not connected") if connection.nil?
415
+ refresh_hosts_async.fallback do |e|
416
+ case e
417
+ when Errors::HostError
418
+ refresh_hosts_async_retry(e, @reconnection_policy.schedule)
419
+ else
420
+ connection = @connection
421
+ connection && connection.close(e)
263
422
 
264
- @logger.debug('Fetching cluster metadata and peers')
423
+ Ione::Future.resolved
424
+ end
425
+ end.map do
426
+ synchronize do
427
+ @refreshing_hosts = false
428
+ end
429
+ end
430
+ end
265
431
 
266
- local = connection.send_request(SELECT_LOCAL)
267
- peers = connection.send_request(SELECT_PEERS)
432
+ def refresh_hosts_async_retry(error, schedule)
433
+ timeout = schedule.next
434
+ @logger.info("Failed to refresh hosts (#{error.class.name}: #{error.message}), retrying in #{timeout}")
435
+
436
+ timer = @io_reactor.schedule_timer(timeout)
437
+ timer.flat_map do
438
+ refresh_hosts_async.fallback do |e|
439
+ case e
440
+ when Errors::HostError
441
+ refresh_hosts_async_retry(e, schedule)
442
+ else
443
+ connection = @connection
444
+ connection && connection.close(e)
445
+
446
+ Ione::Future.resolved
447
+ end
448
+ end
449
+ end
450
+ end
268
451
 
269
- Ione::Future.all(local, peers).flat_map do |(local, peers)|
270
- local = local.rows
271
- peers = peers.rows
452
+ def refresh_hosts_async
453
+ connection = @connection
272
454
 
273
- @logger.debug('%d peers found' % peers.size)
455
+ return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil?
274
456
 
275
- raise NO_HOSTS if local.empty? && peers.empty?
457
+ local = send_select_request(connection, SELECT_LOCAL)
458
+ peers = send_select_request(connection, SELECT_PEERS)
459
+
460
+ Ione::Future.all(local, peers).map do |(local, peers)|
461
+ @logger.debug("#{peers.size} peer(s) found")
276
462
 
277
463
  ips = ::Set.new
278
464
 
465
+ unless local.empty?
466
+ ips << ip = IPAddr.new(connection.host)
467
+ data = local.first
468
+ @registry.host_found(ip, data)
469
+ @metadata.update(data)
470
+ end
471
+
279
472
  peers.each do |data|
280
473
  ip = peer_ip(data)
281
474
  next unless ip
@@ -283,27 +476,11 @@ module Cassandra
283
476
  @registry.host_found(ip, data)
284
477
  end
285
478
 
286
- ips << ip = IPAddr.new(connection.host)
287
- data = local.first
288
- @registry.host_found(ip, data)
289
-
290
- futures = []
291
-
292
479
  @registry.each_host do |host|
293
- if ips.include?(host.ip)
294
- futures << refresh_host_status(host) if host.down?
295
- else
296
- @registry.host_lost(host.ip)
297
- end
480
+ @registry.host_lost(host.ip) unless ips.include?(host.ip)
298
481
  end
299
482
 
300
- @metadata.update(data)
301
-
302
- if futures.empty?
303
- Ione::Future.resolved
304
- else
305
- Ione::Future.all(*futures)
306
- end
483
+ nil
307
484
  end
308
485
  end
309
486
 
@@ -311,46 +488,87 @@ module Cassandra
311
488
  @connector.refresh_status(host)
312
489
  end
313
490
 
314
- def refresh_host_status_with_retry(host, schedule)
315
- timeout = schedule.next
491
+ def refresh_host_status_with_retry(original_timer, host, schedule)
492
+ timer = nil
316
493
 
317
- @logger.info("Refreshing host status refresh ip=#{host.ip} in #{timeout}")
494
+ synchronize do
495
+ timer = @refreshing_statuses[host]
318
496
 
319
- f = @io_reactor.schedule_timer(timeout)
320
- f.flat_map do
321
- if synchronize { @refreshing_statuses[host] }
322
- refresh_host_status(host).fallback do |e|
323
- refresh_host_status_with_retry(host, schedule)
324
- end
497
+ # host must have been lost/up or timer was rescheduled
498
+ return Ione::Future.resolved if timer.nil? || timer != original_timer
499
+
500
+ timeout = schedule.next
501
+
502
+ @logger.debug("Checking host #{host.ip} in #{timeout} seconds")
503
+
504
+ @refreshing_statuses[host] = timer = @io_reactor.schedule_timer(timeout)
505
+ end
506
+
507
+ timer.on_value do
508
+ refresh_host_status(host).fallback do |e|
509
+ refresh_host_status_with_retry(timer, host, schedule)
510
+ end
511
+ end
512
+ end
513
+
514
+ def refresh_host_async_maybe_retry(host)
515
+ synchronize do
516
+ return Ione::Future.resolved if @refreshing_hosts || @refreshing_host[host]
517
+ @refreshing_host[host] = true
518
+ end
519
+
520
+ refresh_host_async(host).fallback do |e|
521
+ case e
522
+ when Errors::HostError
523
+ refresh_host_async_retry(host, e, @reconnection_policy.schedule)
325
524
  else
525
+ connection = @connection
526
+ connection && connection.close(e)
527
+
326
528
  Ione::Future.resolved
327
529
  end
530
+ end.map do
531
+ synchronize do
532
+ @refreshing_host.delete(host)
533
+ end
534
+ end
535
+ end
536
+
537
+ def refresh_host_async_retry(host, error, schedule)
538
+ timeout = schedule.next
539
+ @logger.info("Failed to refresh host #{host.ip.to_s} (#{error.class.name}: #{error.message}), retrying in #{timeout}")
540
+
541
+ timer = @io_reactor.schedule_timer(timeout)
542
+ timer.flat_map do
543
+ refresh_host_async(host).fallback do |e|
544
+ case e
545
+ when Errors::HostError
546
+ refresh_host_async_retry(host, e, schedule)
547
+ else
548
+ connection = @connection
549
+ connection && connection.close(e)
550
+
551
+ Ione::Future.resolved
552
+ end
553
+ end
328
554
  end
329
555
  end
330
556
 
331
557
  def refresh_host_async(address)
332
558
  connection = @connection
333
- return Ione::Future.failed("not connected") if connection.nil?
559
+ return Ione::Future.failed(Errors::ClientError.new('Not connected')) if connection.nil?
334
560
 
335
561
  ip = address.to_s
336
562
 
337
- @logger.debug('Fetching node information for %s' % ip)
338
-
339
563
  if ip == connection.host
340
564
  request = SELECT_LOCAL
341
565
  else
342
566
  request = Protocol::QueryRequest.new('SELECT rack, data_center, host_id, rpc_address, release_version, tokens FROM system.peers WHERE peer = ?', [address], nil, :one)
343
567
  end
344
568
 
345
- connection.send_request(request).map do |response|
346
- @logger.debug('Fetched node information for %s' % ip)
347
-
348
- rows = response.rows
349
-
569
+ send_select_request(connection, request).map do |rows|
350
570
  unless rows.empty?
351
571
  @registry.host_found(address, rows.first)
352
- host = @registry.host(address)
353
- refresh_host_status(host) if host.down?
354
572
  end
355
573
 
356
574
  self
@@ -359,33 +577,50 @@ module Cassandra
359
577
 
360
578
  def connect_to_first_available(plan, errors = nil)
361
579
  unless plan.has_next?
362
- @logger.warn("Control connection failed")
363
- return Ione::Future.failed(Errors::NoHostsAvailable.new(errors || {}))
580
+ if errors.nil? && synchronize { @refreshing_statuses.empty? }
581
+ @logger.fatal(<<-MSG)
582
+ Control connection failed and is unlikely to recover.
583
+
584
+ This usually means that all hosts are ignored by current load
585
+ balancing policy, most likely because they changed datacenters.
586
+ Reconnections attempts will continue getting scheduled to
587
+ repeat this message in the logs.
588
+ MSG
589
+ end
590
+
591
+ return Ione::Future.failed(Errors::NoHostsAvailable.new(errors))
364
592
  end
365
593
 
366
594
  host = plan.next
367
- @logger.debug("Attempting connection to ip=#{host.ip}")
595
+ @logger.debug("Connecting to #{host.ip}")
596
+
368
597
  f = connect_to_host(host)
369
598
  f = f.flat_map do |connection|
370
599
  synchronize do
371
600
  @status = :connected
372
601
 
373
- @logger.debug("Control connection established ip=#{connection.host}")
374
602
  @connection = connection
375
603
 
376
- connection.on_closed do
604
+ connection.on_closed do |cause|
377
605
  reconnect = false
378
606
 
379
607
  synchronize do
380
- if @status == :closing
381
- @status = :closed
382
- else
383
- @status = :reconnecting
384
- reconnect = true
608
+ if connection == @connection
609
+ if @status == :closing
610
+ @status = :closed
611
+ else
612
+ @status = :reconnecting
613
+ reconnect = true
614
+ end
615
+
616
+ if cause
617
+ @logger.info("Control connection closed (#{cause.class.name}: #{cause.message})")
618
+ else
619
+ @logger.info("Control connection closed")
620
+ end
621
+
622
+ @connection = nil
385
623
  end
386
-
387
- @logger.debug("Control connection closed ip=#{connection.host}")
388
- @connection = nil
389
624
  end
390
625
 
391
626
  reconnect_async(@reconnection_policy.schedule) if reconnect
@@ -394,23 +629,26 @@ module Cassandra
394
629
 
395
630
  register_async
396
631
  end
397
- f = f.flat_map { refresh_hosts_async }
398
- f = f.flat_map { refresh_schema_async }
399
- f.fallback do |error|
400
- if error.is_a?(Errors::AuthenticationError)
401
- Ione::Future.failed(error)
402
- elsif error.is_a?(Errors::QueryError)
403
- if error.code == 0x100
404
- Ione::Future.failed(Errors::AuthenticationError.new(error.message))
405
- else
406
- Ione::Future.failed(error)
407
- end
408
- else
632
+ f = f.flat_map { refresh_hosts_async_maybe_retry }
633
+ f = f.flat_map { refresh_schema_async_maybe_retry }
634
+ f = f.fallback do |error|
635
+ @logger.debug("Connection to #{host.ip} failed (#{error.class.name}: #{error.message})")
636
+
637
+ case error
638
+ when Errors::HostError
409
639
  errors ||= {}
410
640
  errors[host] = error
411
641
  connect_to_first_available(plan, errors)
642
+ else
643
+ Ione::Future.failed(error)
412
644
  end
413
645
  end
646
+
647
+ f.on_complete do |f|
648
+ @logger.info('Control connection established') if f.resolved?
649
+ end
650
+
651
+ f
414
652
  end
415
653
 
416
654
  def connect_to_host(host)
@@ -420,7 +658,21 @@ module Cassandra
420
658
  def peer_ip(data)
421
659
  ip = data['rpc_address']
422
660
  ip = data['peer'] if ip == '0.0.0.0'
423
- ip
661
+
662
+ @address_resolver.resolve(ip)
663
+ end
664
+
665
+ def send_select_request(connection, request)
666
+ connection.send_request(request).map do |r|
667
+ case r
668
+ when Protocol::RowsResultResponse
669
+ r.rows
670
+ when Protocol::ErrorResponse
671
+ raise r.to_error(VOID_STATEMENT)
672
+ else
673
+ raise Errors::InternalError, "Unexpected response #{r.inspect}"
674
+ end
675
+ end
424
676
  end
425
677
  end
426
678
  end