cql-rb 1.1.0.pre0 → 1.1.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)