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.
@@ -8,6 +8,7 @@ module Cql
8
8
  # A wrapper around a socket. Handles connecting to the remote host, reading
9
9
  # from and writing to the socket.
10
10
  #
11
+ # @private
11
12
  class Connection
12
13
  attr_reader :host, :port, :connection_timeout
13
14
 
@@ -78,6 +78,8 @@ module Cql
78
78
  # protocol is implemented, and there is an integration tests that implements
79
79
  # the Redis protocol that you can look at too.
80
80
  #
81
+ # @private
82
+ #
81
83
  class IoReactor
82
84
  # Initializes a new IO reactor.
83
85
  #
@@ -10,7 +10,7 @@ module Cql
10
10
  #
11
11
  # Instances of this class are thread safe.
12
12
  #
13
- # @examle Sending an OPTIONS request
13
+ # @example Sending an OPTIONS request
14
14
  # future = protocol_handler.send_request(Cql::Protocol::OptionsRequest.new)
15
15
  # response = future.get
16
16
  # puts "These options are supported: #{response.options}"
@@ -29,11 +29,26 @@ module Cql
29
29
  @request_queue_in = []
30
30
  @request_queue_out = []
31
31
  @event_listeners = []
32
+ @data = {}
32
33
  @lock = Mutex.new
33
34
  @closed_future = Future.new
34
35
  @keyspace = nil
35
36
  end
36
37
 
38
+ # Associate arbitrary data with this protocol handler object. This is
39
+ # useful in situations where additional metadata can be loaded after the
40
+ # connection has been set up, or to keep statistics specific to the
41
+ # connection this protocol handler wraps.
42
+ def []=(key, value)
43
+ @lock.synchronize { @data[key] = value }
44
+ end
45
+
46
+ # @see {#[]=}
47
+ # @return the value associated with the key
48
+ def [](key)
49
+ @lock.synchronize { @data[key] }
50
+ end
51
+
37
52
  # @return [true, false] true if the underlying connection is connected
38
53
  def connected?
39
54
  @connection.connected?
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
- VERSION = '1.1.0.pre0'.freeze
4
+ VERSION = '1.1.0.pre1'.freeze
5
5
  end
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'spec_helper'
4
- require 'cql/client/client_shared'
5
4
 
6
5
 
7
6
  module Cql
@@ -11,7 +10,48 @@ module Cql
11
10
  described_class.new(connection_options)
12
11
  end
13
12
 
14
- include_context 'client setup'
13
+ let :connection_options do
14
+ {:host => 'example.com', :port => 12321, :io_reactor => io_reactor}
15
+ end
16
+
17
+ let :io_reactor do
18
+ FakeIoReactor.new
19
+ end
20
+
21
+ def connections
22
+ io_reactor.connections
23
+ end
24
+
25
+ def last_connection
26
+ connections.last
27
+ end
28
+
29
+ def requests
30
+ last_connection.requests
31
+ end
32
+
33
+ def last_request
34
+ requests.last
35
+ end
36
+
37
+ def handle_request(&handler)
38
+ @request_handler = handler
39
+ end
40
+
41
+ before do
42
+ io_reactor.on_connection do |connection|
43
+ connection.handle_request do |request|
44
+ response = nil
45
+ if @request_handler
46
+ response = @request_handler.call(request, connection, proc { connection.default_request_handler(request) })
47
+ end
48
+ unless response
49
+ response = connection.default_request_handler(request)
50
+ end
51
+ response
52
+ end
53
+ end
54
+ end
15
55
 
16
56
  describe '#connect' do
17
57
  it 'connects' do
@@ -25,14 +65,45 @@ module Cql
25
65
  connections.should have(1).item
26
66
  end
27
67
 
28
- it 'connects to all hosts' do
29
- client.close.get
30
- io_reactor.stop.get
31
- io_reactor.start.get
68
+ context 'when connecting to multiple hosts' do
69
+ before do
70
+ client.close.get
71
+ io_reactor.stop.get
72
+ end
32
73
 
33
- c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
34
- c.connect.get
35
- connections.should have(3).items
74
+ it 'connects to all hosts' do
75
+ c = described_class.new(connection_options.merge(hosts: %w[h1.example.com h2.example.com h3.example.com]))
76
+ c.connect.get
77
+ connections.should have(3).items
78
+ end
79
+
80
+ it 'connects to all hosts, when given as a comma-sepatated string' do
81
+ c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
82
+ c.connect.get
83
+ connections.should have(3).items
84
+ end
85
+
86
+ it 'only connects to each host once' do
87
+ c = described_class.new(connection_options.merge(hosts: %w[h1.example.com h2.example.com h2.example.com]))
88
+ c.connect.get
89
+ connections.should have(2).items
90
+ end
91
+
92
+ it 'succeeds even if only one of the connections succeeded' do
93
+ io_reactor.node_down('h1.example.com')
94
+ io_reactor.node_down('h3.example.com')
95
+ c = described_class.new(connection_options.merge(hosts: %w[h1.example.com h2.example.com h2.example.com]))
96
+ c.connect.get
97
+ connections.should have(1).items
98
+ end
99
+
100
+ it 'fails when all nodes are down' do
101
+ io_reactor.node_down('h1.example.com')
102
+ io_reactor.node_down('h2.example.com')
103
+ io_reactor.node_down('h3.example.com')
104
+ c = described_class.new(connection_options.merge(hosts: %w[h1.example.com h2.example.com h2.example.com]))
105
+ expect { c.connect.get }.to raise_error(Io::ConnectionError)
106
+ end
36
107
  end
37
108
 
38
109
  it 'returns itself' do
@@ -52,7 +123,7 @@ module Cql
52
123
 
53
124
  it 'sends a startup request' do
54
125
  client.connect.get
55
- last_request.should be_a(Protocol::StartupRequest)
126
+ requests.first.should be_a(Protocol::StartupRequest)
56
127
  end
57
128
 
58
129
  it 'sends a startup request to each connection' do
@@ -60,10 +131,10 @@ module Cql
60
131
  io_reactor.stop.get
61
132
  io_reactor.start.get
62
133
 
63
- c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
134
+ c = described_class.new(connection_options.merge(hosts: %w[h1.example.com h2.example.com h3.example.com]))
64
135
  c.connect.get
65
136
  connections.each do |cc|
66
- cc.requests.last.should be_a(Protocol::StartupRequest)
137
+ cc.requests.first.should be_a(Protocol::StartupRequest)
67
138
  end
68
139
  end
69
140
 
@@ -75,7 +146,8 @@ module Cql
75
146
  it 'changes to the keyspace given as an option' do
76
147
  c = described_class.new(connection_options.merge(:keyspace => 'hello_world'))
77
148
  c.connect.get
78
- last_request.should == Protocol::QueryRequest.new('USE hello_world', :one)
149
+ request = requests.find { |rq| rq == Protocol::QueryRequest.new('USE hello_world', :one) }
150
+ request.should_not be_nil, 'expected a USE request to have been sent'
79
151
  end
80
152
 
81
153
  it 'validates the keyspace name before sending the USE command' do
@@ -84,6 +156,107 @@ module Cql
84
156
  requests.should_not include(Protocol::QueryRequest.new('USE system; DROP KEYSPACE system', :one))
85
157
  end
86
158
 
159
+ context 'with automatic peer discovery' do
160
+ let :local_info do
161
+ {
162
+ 'data_center' => 'dc1',
163
+ 'host_id' => nil,
164
+ }
165
+ end
166
+
167
+ let :local_metadata do
168
+ [
169
+ ['system', 'local', 'data_center', :text],
170
+ ['system', 'local', 'host_id', :uuid],
171
+ ]
172
+ end
173
+
174
+ let :peer_metadata do
175
+ [
176
+ ['system', 'peers', 'peer', :inet],
177
+ ['system', 'peers', 'data_center', :varchar],
178
+ ['system', 'peers', 'host_id', :uuid],
179
+ ['system', 'peers', 'rpc_address', :inet],
180
+ ]
181
+ end
182
+
183
+ let :data_centers do
184
+ Hash.new('dc1')
185
+ end
186
+
187
+ let :additional_nodes do
188
+ Array.new(5) { IPAddr.new("127.0.#{rand(255)}.#{rand(255)}") }
189
+ end
190
+
191
+ before do
192
+ uuid_generator = TimeUuid::Generator.new
193
+ additional_rpc_addresses = additional_nodes.dup
194
+ io_reactor.on_connection do |connection|
195
+ connection[:spec_host_id] = uuid_generator.next
196
+ connection[:spec_data_center] = data_centers[connection.host]
197
+ connection.handle_request do |request|
198
+ case request
199
+ when Protocol::StartupRequest
200
+ Protocol::ReadyResponse.new
201
+ when Protocol::QueryRequest
202
+ case request.cql
203
+ when /FROM system\.local/
204
+ row = {'host_id' => connection[:spec_host_id], 'data_center' => connection[:spec_data_center]}
205
+ Protocol::RowsResultResponse.new([row], local_metadata)
206
+ when /FROM system\.peers/
207
+ other_host_ids = connections.reject { |c| c[:spec_host_id] == connection[:spec_host_id] }.map { |c| c[:spec_host_id] }
208
+ until other_host_ids.size >= 2
209
+ other_host_ids << uuid_generator.next
210
+ end
211
+ rows = other_host_ids.map do |host_id|
212
+ ip = additional_rpc_addresses.shift
213
+ {'host_id' => host_id, 'data_center' => data_centers[ip], 'rpc_address' => ip}
214
+ end
215
+ Protocol::RowsResultResponse.new(rows, peer_metadata)
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ it 'connects to the other nodes in the cluster' do
223
+ client.connect.get
224
+ connections.should have(3).items
225
+ end
226
+
227
+ it 'connects to the other nodes in the same data center' do
228
+ data_centers[additional_nodes[1]] = 'dc2'
229
+ client.connect.get
230
+ connections.should have(2).items
231
+ end
232
+
233
+ it 'connects to the other nodes in same data centers as the seed nodes' do
234
+ data_centers['host2'] = 'dc2'
235
+ data_centers[additional_nodes[1]] = 'dc2'
236
+ c = described_class.new(connection_options.merge(hosts: %w[host1 host2]))
237
+ c.connect.get
238
+ connections.should have(3).items
239
+ end
240
+
241
+ it 'only connects to the other nodes in the cluster it is not already connected do' do
242
+ c = described_class.new(connection_options.merge(hosts: %w[host1 host2]))
243
+ c.connect.get
244
+ connections.should have(3).items
245
+ end
246
+
247
+ it 'handles the case when it is already connected to all nodes' do
248
+ c = described_class.new(connection_options.merge(hosts: %w[host1 host2 host3 host4]))
249
+ c.connect.get
250
+ connections.should have(4).items
251
+ end
252
+
253
+ it 'accepts that some nodes are down' do
254
+ io_reactor.node_down(additional_nodes.first.to_s)
255
+ client.connect.get
256
+ connections.should have(2).items
257
+ end
258
+ end
259
+
87
260
  it 're-raises any errors raised' do
88
261
  io_reactor.stub(:connect).and_raise(ArgumentError)
89
262
  expect { client.connect.get }.to raise_error(ArgumentError)
@@ -102,24 +275,45 @@ module Cql
102
275
  end
103
276
 
104
277
  it 'is not connected while connecting' do
278
+ go = false
105
279
  io_reactor.stop.get
106
- f = client.connect
107
- client.should_not be_connected
108
- io_reactor.start.get
109
- f.get
280
+ io_reactor.before_startup { sleep 0.01 until go }
281
+ client.connect
282
+ begin
283
+ client.should_not be_connected
284
+ ensure
285
+ go = true
286
+ end
110
287
  end
111
288
 
112
289
  context 'when the server requests authentication' do
113
- before do
114
- io_reactor.on_connection do |connection|
115
- connection.queue_response(Protocol::AuthenticateResponse.new('com.example.Auth'))
290
+ def accepting_request_handler(request, *)
291
+ case request
292
+ when Protocol::StartupRequest
293
+ Protocol::AuthenticateResponse.new('com.example.Auth')
294
+ when Protocol::CredentialsRequest
295
+ Protocol::ReadyResponse.new
116
296
  end
117
297
  end
118
298
 
299
+ def denying_request_handler(request, *)
300
+ case request
301
+ when Protocol::StartupRequest
302
+ Protocol::AuthenticateResponse.new('com.example.Auth')
303
+ when Protocol::CredentialsRequest
304
+ Protocol::ErrorResponse.new(256, 'No way, José')
305
+ end
306
+ end
307
+
308
+ before do
309
+ handle_request(&method(:accepting_request_handler))
310
+ end
311
+
119
312
  it 'sends credentials' do
120
313
  client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
121
314
  client.connect.get
122
- last_request.should == Protocol::CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
315
+ request = requests.find { |rq| rq == Protocol::CredentialsRequest.new('username' => 'foo', 'password' => 'bar') }
316
+ request.should_not be_nil, 'expected a credentials request to have been sent'
123
317
  end
124
318
 
125
319
  it 'raises an error when no credentials have been given' do
@@ -128,17 +322,13 @@ module Cql
128
322
  end
129
323
 
130
324
  it 'raises an error when the server responds with an error to the credentials request' do
131
- io_reactor.on_connection do |connection|
132
- connection.queue_response(Protocol::ErrorResponse.new(256, 'No way, José'))
133
- end
325
+ handle_request(&method(:denying_request_handler))
134
326
  client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
135
327
  expect { client.connect.get }.to raise_error(AuthenticationError)
136
328
  end
137
329
 
138
330
  it 'shuts down the client when there is an authentication error' do
139
- io_reactor.on_connection do |connection|
140
- connection.queue_response(Protocol::ErrorResponse.new(256, 'No way, José'))
141
- end
331
+ handle_request(&method(:denying_request_handler))
142
332
  client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
143
333
  client.connect.get rescue nil
144
334
  client.should_not be_connected
@@ -175,14 +365,13 @@ module Cql
175
365
  end
176
366
 
177
367
  describe '#use' do
178
- before do
179
- client.connect.get
180
- end
181
-
182
368
  it 'executes a USE query' do
183
- io_reactor.on_connection do |connection|
184
- connection.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
369
+ handle_request do |request|
370
+ if request.is_a?(Protocol::QueryRequest) && request.cql == 'USE system'
371
+ Protocol::SetKeyspaceResultResponse.new('system')
372
+ end
185
373
  end
374
+ client.connect.get
186
375
  client.use('system').get
187
376
  last_request.should == Protocol::QueryRequest.new('USE system', :one)
188
377
  end
@@ -192,7 +381,7 @@ module Cql
192
381
  io_reactor.stop.get
193
382
  io_reactor.start.get
194
383
 
195
- c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
384
+ c = described_class.new(connection_options.merge(hosts: %w[h1.example.com h2.example.com h3.example.com]))
196
385
  c.connect.get
197
386
 
198
387
  c.use('system').get
@@ -205,12 +394,18 @@ module Cql
205
394
  end
206
395
 
207
396
  it 'knows which keyspace it changed to' do
208
- last_connection.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
397
+ handle_request do |request|
398
+ if request.is_a?(Protocol::QueryRequest) && request.cql == 'USE system'
399
+ Protocol::SetKeyspaceResultResponse.new('system')
400
+ end
401
+ end
402
+ client.connect.get
209
403
  client.use('system').get
210
404
  client.keyspace.should == 'system'
211
405
  end
212
406
 
213
407
  it 'raises an error if the keyspace name is not valid' do
408
+ client.connect.get
214
409
  expect { client.use('system; DROP KEYSPACE system').get }.to raise_error(InvalidKeyspaceNameError)
215
410
  end
216
411
  end
@@ -232,7 +427,11 @@ module Cql
232
427
 
233
428
  context 'with a void CQL query' do
234
429
  it 'returns nil' do
235
- last_connection.queue_response(Protocol::VoidResultResponse.new)
430
+ handle_request do |request|
431
+ if request.is_a?(Protocol::QueryRequest) && request.cql =~ /UPDATE/
432
+ Protocol::VoidResultResponse.new
433
+ end
434
+ end
236
435
  result = client.execute('UPDATE stuff SET thing = 1 WHERE id = 3').get
237
436
  result.should be_nil
238
437
  end
@@ -240,13 +439,21 @@ module Cql
240
439
 
241
440
  context 'with a USE query' do
242
441
  it 'returns nil' do
243
- last_connection.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
442
+ handle_request do |request|
443
+ if request.is_a?(Protocol::QueryRequest) && request.cql == 'USE system'
444
+ Protocol::SetKeyspaceResultResponse.new('system')
445
+ end
446
+ end
244
447
  result = client.execute('USE system').get
245
448
  result.should be_nil
246
449
  end
247
450
 
248
451
  it 'knows which keyspace it changed to' do
249
- last_connection.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
452
+ handle_request do |request|
453
+ if request.is_a?(Protocol::QueryRequest) && request.cql == 'USE system'
454
+ Protocol::SetKeyspaceResultResponse.new('system')
455
+ end
456
+ end
250
457
  client.execute('USE system').get
251
458
  client.keyspace.should == 'system'
252
459
  end
@@ -256,12 +463,14 @@ module Cql
256
463
  io_reactor.stop.get
257
464
  io_reactor.start.get
258
465
 
259
- c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
260
- c.connect.get
466
+ handle_request do |request, connection|
467
+ if request.is_a?(Protocol::QueryRequest) && request.cql == 'USE system'
468
+ Protocol::SetKeyspaceResultResponse.new('system')
469
+ end
470
+ end
261
471
 
262
- connections.find { |c| c.host == 'h1.example.com' }.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
263
- connections.find { |c| c.host == 'h2.example.com' }.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
264
- connections.find { |c| c.host == 'h3.example.com' }.queue_response(Protocol::SetKeyspaceResultResponse.new('system'))
472
+ c = described_class.new(connection_options.merge(hosts: %w[h1.example.com h2.example.com h3.example.com]))
473
+ c.connect.get
265
474
 
266
475
  c.execute('USE system', :one).get
267
476
  c.keyspace.should == 'system'
@@ -285,10 +494,17 @@ module Cql
285
494
  end
286
495
 
287
496
  let :result do
288
- last_connection.queue_response(Protocol::RowsResultResponse.new(rows, metadata))
289
497
  client.execute('SELECT * FROM things').get
290
498
  end
291
499
 
500
+ before do
501
+ handle_request do |request|
502
+ if request.is_a?(Protocol::QueryRequest) && request.cql =~ /FROM things/
503
+ Protocol::RowsResultResponse.new(rows, metadata)
504
+ end
505
+ end
506
+ end
507
+
292
508
  it 'returns an Enumerable of rows' do
293
509
  row_count = 0
294
510
  result.each do |row|
@@ -327,13 +543,19 @@ module Cql
327
543
  end
328
544
 
329
545
  context 'when the response is an error' do
546
+ before do
547
+ handle_request do |request|
548
+ if request.is_a?(Protocol::QueryRequest) && request.cql =~ /FROM things/
549
+ Protocol::ErrorResponse.new(0xabcd, 'Blurgh')
550
+ end
551
+ end
552
+ end
553
+
330
554
  it 'raises an error' do
331
- last_connection.queue_response(Protocol::ErrorResponse.new(0xabcd, 'Blurgh'))
332
555
  expect { client.execute('SELECT * FROM things').get }.to raise_error(QueryError, 'Blurgh')
333
556
  end
334
557
 
335
558
  it 'decorates the error with the CQL that caused it' do
336
- last_connection.queue_response(Protocol::ErrorResponse.new(0xabcd, 'Blurgh'))
337
559
  begin
338
560
  client.execute('SELECT * FROM things').get
339
561
  rescue QueryError => e
@@ -354,6 +576,14 @@ module Cql
354
576
  [['stuff', 'things', 'item', :varchar]]
355
577
  end
356
578
 
579
+ before do
580
+ handle_request do |request|
581
+ if request.is_a?(Protocol::PrepareRequest)
582
+ Protocol::PreparedResultResponse.new(id, metadata)
583
+ end
584
+ end
585
+ end
586
+
357
587
  before do
358
588
  client.connect.get
359
589
  end
@@ -364,26 +594,22 @@ module Cql
364
594
  end
365
595
 
366
596
  it 'returns a prepared statement' do
367
- last_connection.queue_response(Protocol::PreparedResultResponse.new('A' * 32, [['stuff', 'things', 'item', :varchar]]))
368
597
  statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?').get
369
598
  statement.should_not be_nil
370
599
  end
371
600
 
372
601
  it 'executes a prepared statement' do
373
- last_connection.queue_response(Protocol::PreparedResultResponse.new(id, metadata))
374
602
  statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?').get
375
603
  statement.execute('foo').get
376
604
  last_request.should == Protocol::ExecuteRequest.new(id, metadata, ['foo'], :quorum)
377
605
  end
378
606
 
379
607
  it 'returns a prepared statement that knows the metadata' do
380
- last_connection.queue_response(Protocol::PreparedResultResponse.new(id, metadata))
381
608
  statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?').get
382
609
  statement.metadata['item'].type == :varchar
383
610
  end
384
611
 
385
612
  it 'executes a prepared statement with a specific consistency level' do
386
- last_connection.queue_response(Protocol::PreparedResultResponse.new(id, metadata))
387
613
  statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?').get
388
614
  statement.execute('thing', :local_quorum).get
389
615
  last_request.should == Protocol::ExecuteRequest.new(id, metadata, ['thing'], :local_quorum)
@@ -398,7 +624,11 @@ module Cql
398
624
 
399
625
  context 'when there is an error preparing the request' do
400
626
  it 'returns a failed future' do
401
- last_connection.queue_response(Protocol::PreparedResultResponse.new(id, metadata))
627
+ handle_request do |request|
628
+ if request.is_a?(Protocol::PrepareRequest)
629
+ Protocol::PreparedResultResponse.new(id, metadata)
630
+ end
631
+ end
402
632
  statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?').get
403
633
  f = statement.execute
404
634
  expect { f.get }.to raise_error(ArgumentError)
@@ -448,8 +678,12 @@ module Cql
448
678
  end
449
679
 
450
680
  it 'complains when #execute of a prepared statement is called after #close' do
681
+ handle_request do |request|
682
+ if request.is_a?(Protocol::PrepareRequest)
683
+ Protocol::PreparedResultResponse.new('A' * 32, [])
684
+ end
685
+ end
451
686
  client.connect.get
452
- last_connection.queue_response(Protocol::PreparedResultResponse.new('A' * 32, []))
453
687
  statement = client.prepare('DELETE FROM stuff WHERE id = 3').get
454
688
  client.close.get
455
689
  expect { statement.execute.get }.to raise_error(NotConnectedError)