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

Sign up to get free protection for your applications and to get access to all the features.
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