cql-rb 1.0.0.pre4 → 1.0.0.pre5

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.
@@ -0,0 +1,206 @@
1
+ # encoding: utf-8
2
+
3
+ require 'socket'
4
+ require 'resolv-replace'
5
+
6
+
7
+ module Cql
8
+ module Io
9
+ # @private
10
+ class NodeConnection
11
+ def initialize(*args)
12
+ @host, @port, @connection_timeout = args
13
+ @connected_future = Future.new
14
+ @io = nil
15
+ @addrinfo = nil
16
+ @write_buffer = ''
17
+ @read_buffer = ''
18
+ @current_frame = Protocol::ResponseFrame.new(@read_buffer)
19
+ @response_tasks = [nil] * 128
20
+ @event_listeners = Hash.new { |h, k| h[k] = [] }
21
+ end
22
+
23
+ def open
24
+ @connection_started_at = Time.now
25
+ begin
26
+ addrinfo = Socket.getaddrinfo(@host, @port, Socket::AF_INET, Socket::SOCK_STREAM)
27
+ _, port, _, ip, address_family, socket_type = addrinfo.first
28
+ @sockaddr = Socket.sockaddr_in(port, ip)
29
+ @io = Socket.new(address_family, socket_type, 0)
30
+ @io.connect_nonblock(@sockaddr)
31
+ rescue Errno::EINPROGRESS
32
+ # ok
33
+ rescue SystemCallError, SocketError => e
34
+ fail_connection!(e)
35
+ end
36
+ @connected_future
37
+ end
38
+
39
+ def connection_id
40
+ self.object_id
41
+ end
42
+
43
+ def to_io
44
+ @io
45
+ end
46
+
47
+ def on_event(&listener)
48
+ @event_listeners[:event] << listener
49
+ end
50
+
51
+ def on_close(&listener)
52
+ @event_listeners[:close] << listener
53
+ end
54
+
55
+ def connected?
56
+ @io && !connecting?
57
+ end
58
+
59
+ def connecting?
60
+ @io && !(@connected_future.complete? || @connected_future.failed?)
61
+ end
62
+
63
+ def closed?
64
+ @io.nil? && !connecting?
65
+ end
66
+
67
+ def has_capacity?
68
+ !!next_stream_id && connected?
69
+ end
70
+
71
+ def can_write?
72
+ @io && (!@write_buffer.empty? || connecting?)
73
+ end
74
+
75
+ def perform_request(request, future)
76
+ stream_id = next_stream_id
77
+ Protocol::RequestFrame.new(request, stream_id).write(@write_buffer)
78
+ @response_tasks[stream_id] = future
79
+ rescue => e
80
+ case e
81
+ when CqlError
82
+ error = e
83
+ else
84
+ error = IoError.new(e.message)
85
+ error.set_backtrace(e.backtrace)
86
+ end
87
+ @response_tasks.delete(stream_id)
88
+ future.fail!(error)
89
+ end
90
+
91
+ def handle_read
92
+ new_bytes = @io.read_nonblock(2**16)
93
+ @current_frame << new_bytes
94
+ while @current_frame.complete?
95
+ stream_id = @current_frame.stream_id
96
+ if stream_id == EVENT_STREAM_ID
97
+ @event_listeners[:event].each { |listener| listener.call(@current_frame.body) }
98
+ elsif @response_tasks[stream_id]
99
+ @response_tasks[stream_id].complete!([@current_frame.body, connection_id])
100
+ @response_tasks[stream_id] = nil
101
+ else
102
+ # TODO dropping the request on the floor here, but we didn't send it
103
+ end
104
+ @current_frame = Protocol::ResponseFrame.new(@read_buffer)
105
+ end
106
+ rescue => e
107
+ force_close(e)
108
+ end
109
+
110
+ def handle_write
111
+ succeed_connection! if connecting?
112
+ if !@write_buffer.empty?
113
+ bytes_written = @io.write_nonblock(@write_buffer)
114
+ @write_buffer.slice!(0, bytes_written)
115
+ end
116
+ rescue => e
117
+ force_close(e)
118
+ end
119
+
120
+ def handle_connecting
121
+ if connecting_timed_out?
122
+ fail_connection!(ConnectionTimeoutError.new("Could not connect to #{@host}:#{@port} within #{@connection_timeout}s"))
123
+ else
124
+ @io.connect_nonblock(@sockaddr)
125
+ succeed_connection!
126
+ end
127
+ rescue Errno::EISCONN
128
+ # ok
129
+ succeed_connection!
130
+ rescue SystemCallError, SocketError => e
131
+ fail_connection!(e)
132
+ end
133
+
134
+ def close
135
+ if @io
136
+ begin
137
+ @io.close
138
+ rescue SystemCallError
139
+ # nothing to do, it wasn't open
140
+ end
141
+ if connecting?
142
+ succeed_connection!
143
+ end
144
+ @io = nil
145
+ @event_listeners[:close].each { |listener| listener.call(self) }
146
+ end
147
+ end
148
+
149
+ def to_s
150
+ state = begin
151
+ if connected? then 'connected'
152
+ elsif connecting? then 'connecting'
153
+ else 'not connected'
154
+ end
155
+ end
156
+ %<NodeConnection(#{@host}:#{@port}, #{state})>
157
+ end
158
+
159
+ private
160
+
161
+ EVENT_STREAM_ID = -1
162
+
163
+ def connecting_timed_out?
164
+ (Time.now - @connection_started_at) > @connection_timeout
165
+ end
166
+
167
+ def succeed_connection!
168
+ @connected_future.complete!(connection_id)
169
+ end
170
+
171
+ def fail_connection!(e)
172
+ case e
173
+ when ConnectionError
174
+ error = e
175
+ else
176
+ message = "Could not connect to #{@host}:#{@port}: #{e.message} (#{e.class.name})"
177
+ error = ConnectionError.new(message)
178
+ error.set_backtrace(e.backtrace)
179
+ end
180
+ @connected_future.fail!(error)
181
+ force_close(error)
182
+ end
183
+
184
+ def force_close(e)
185
+ case e
186
+ when CqlError
187
+ error = e
188
+ else
189
+ error = IoError.new(e.message)
190
+ error.set_backtrace(e.backtrace)
191
+ end
192
+ @response_tasks.each do |listener|
193
+ listener.fail!(error) if listener
194
+ end
195
+ close
196
+ end
197
+
198
+ def next_stream_id
199
+ @response_tasks.each_with_index do |task, index|
200
+ return index if task.nil?
201
+ end
202
+ nil
203
+ end
204
+ end
205
+ end
206
+ end
@@ -14,6 +14,7 @@ module Cql
14
14
  end
15
15
 
16
16
  def write_string(buffer, str)
17
+ str = str.to_s
17
18
  buffer << [str.bytesize].pack(Formats::SHORT_FORMAT)
18
19
  buffer << binary_cast(str)
19
20
  buffer.force_encoding(::Encoding::BINARY)
@@ -49,6 +49,32 @@ module Cql
49
49
  COMPRESSION = 'COMPRESSION'.freeze
50
50
  end
51
51
 
52
+ class CredentialsRequest < RequestBody
53
+ attr_reader :credentials
54
+
55
+ def initialize(credentials)
56
+ super(4)
57
+ @credentials = credentials.dup.freeze
58
+ end
59
+
60
+ def write(io)
61
+ write_string_map(io, @credentials)
62
+ end
63
+
64
+ def to_s
65
+ %(CREDENTIALS #{@credentials})
66
+ end
67
+
68
+ def eql?(rq)
69
+ self.class === rq && rq.credentials.eql?(@credentials)
70
+ end
71
+ alias_method :==, :eql?
72
+
73
+ def hash
74
+ @h ||= @credentials.hash
75
+ end
76
+ end
77
+
52
78
  class OptionsRequest < RequestBody
53
79
  def initialize
54
80
  super(5)
@@ -55,6 +55,7 @@ module Cql
55
55
  case @headers.opcode
56
56
  when 0x00 then ErrorResponse
57
57
  when 0x02 then ReadyResponse
58
+ when 0x03 then AuthenticateResponse
58
59
  when 0x06 then SupportedResponse
59
60
  when 0x08 then ResultResponse
60
61
  when 0x0c then EventResponse
@@ -214,6 +215,22 @@ module Cql
214
215
  end
215
216
  end
216
217
 
218
+ class AuthenticateResponse < ResponseBody
219
+ attr_reader :authentication_class
220
+
221
+ def self.decode!(buffer)
222
+ new(read_string!(buffer))
223
+ end
224
+
225
+ def initialize(authentication_class)
226
+ @authentication_class = authentication_class
227
+ end
228
+
229
+ def to_s
230
+ %(AUTHENTICATE #{authentication_class})
231
+ end
232
+ end
233
+
217
234
  class SupportedResponse < ResponseBody
218
235
  attr_reader :options
219
236
 
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cql
4
- VERSION = '1.0.0.pre4'.freeze
4
+ VERSION = '1.0.0.pre5'.freeze
5
5
  end
@@ -33,114 +33,153 @@ module Cql
33
33
  requests.last
34
34
  end
35
35
 
36
- describe '#start!' do
36
+ describe '.connect' do
37
+ it 'connects and returns the client' do
38
+ client = described_class.connect(connection_options)
39
+ client.should be_connected
40
+ end
41
+ end
42
+
43
+ describe '#connect' do
37
44
  it 'connects' do
38
- client.start!
45
+ client.connect
39
46
  connections.should have(1).item
40
47
  end
41
48
 
42
49
  it 'connects only once' do
43
- client.start!
44
- client.start!
50
+ client.connect
51
+ client.connect
45
52
  connections.should have(1).item
46
53
  end
47
54
 
48
55
  it 'connects to all hosts' do
49
- client.shutdown!
56
+ client.close
50
57
  io_reactor.stop.get
51
58
  io_reactor.start.get
52
59
 
53
60
  c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
54
- c.start!
61
+ c.connect
55
62
  connections.should have(3).items
56
63
  end
57
64
 
58
65
  it 'returns itself' do
59
- client.start!.should equal(client)
66
+ client.connect.should equal(client)
60
67
  end
61
68
 
62
69
  it 'forwards the host and port' do
63
- client.start!
70
+ client.connect
64
71
  connection[:host].should == 'example.com'
65
72
  connection[:port].should == 12321
66
73
  end
67
74
 
68
75
  it 'sends a startup request' do
69
- client.start!
76
+ client.connect
70
77
  last_request.should be_a(Protocol::StartupRequest)
71
78
  end
72
79
 
73
80
  it 'sends a startup request to each connection' do
74
- client.shutdown!
81
+ client.close
75
82
  io_reactor.stop.get
76
83
  io_reactor.start.get
77
84
 
78
85
  c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
79
- c.start!
86
+ c.connect
80
87
  connections.each do |cc|
81
88
  cc[:requests].last.should be_a(Protocol::StartupRequest)
82
89
  end
83
90
  end
84
91
 
85
92
  it 'is not in a keyspace' do
86
- client.start!
93
+ client.connect
87
94
  client.keyspace.should be_nil
88
95
  end
89
96
 
90
97
  it 'changes to the keyspace given as an option' do
91
98
  c = described_class.new(connection_options.merge(:keyspace => 'hello_world'))
92
- c.start!
99
+ c.connect
93
100
  last_request.should == Protocol::QueryRequest.new('USE hello_world', :one)
94
101
  end
95
102
 
96
103
  it 'validates the keyspace name before sending the USE command' do
97
104
  c = described_class.new(connection_options.merge(:keyspace => 'system; DROP KEYSPACE system'))
98
- expect { c.start! }.to raise_error(Client::InvalidKeyspaceNameError)
105
+ expect { c.connect }.to raise_error(Client::InvalidKeyspaceNameError)
99
106
  requests.should_not include(Protocol::QueryRequest.new('USE system; DROP KEYSPACE system', :one))
100
107
  end
101
108
 
102
109
  it 're-raises any errors raised' do
103
110
  io_reactor.stub(:add_connection).and_raise(ArgumentError)
104
- expect { client.start! }.to raise_error(ArgumentError)
111
+ expect { client.connect }.to raise_error(ArgumentError)
105
112
  end
106
113
 
107
114
  it 'is not connected if an error is raised' do
108
115
  io_reactor.stub(:add_connection).and_raise(ArgumentError)
109
- client.start! rescue nil
116
+ client.connect rescue nil
110
117
  client.should_not be_connected
118
+ io_reactor.should_not be_running
111
119
  end
112
120
 
113
- it 'is connected after #start! returns' do
114
- client.start!
121
+ it 'is connected after #connect returns' do
122
+ client.connect
115
123
  client.should be_connected
116
124
  end
125
+
126
+ context 'when the server requests authentication' do
127
+ before do
128
+ io_reactor.queue_response(Protocol::AuthenticateResponse.new('com.example.Auth'))
129
+ end
130
+
131
+ it 'sends credentials' do
132
+ client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
133
+ client.connect
134
+ last_request.should == Protocol::CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
135
+ end
136
+
137
+ it 'raises an error when no credentials have been given' do
138
+ client = described_class.new(connection_options)
139
+ expect { client.connect }.to raise_error(AuthenticationError)
140
+ end
141
+
142
+ it 'raises an error when the server responds with an error to the credentials request' do
143
+ io_reactor.queue_response(Protocol::ErrorResponse.new(256, 'No way, José'))
144
+ client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
145
+ expect { client.connect }.to raise_error(AuthenticationError)
146
+ end
147
+
148
+ it 'shuts down the client when there is an authentication error' do
149
+ io_reactor.queue_response(Protocol::ErrorResponse.new(256, 'No way, José'))
150
+ client = described_class.new(connection_options.merge(credentials: {'username' => 'foo', 'password' => 'bar'}))
151
+ client.connect rescue nil
152
+ client.should_not be_connected
153
+ io_reactor.should_not be_running
154
+ end
155
+ end
117
156
  end
118
157
 
119
- describe '#shutdown!' do
158
+ describe '#close' do
120
159
  it 'closes the connection' do
121
- client.start!
122
- client.shutdown!
160
+ client.connect
161
+ client.close
123
162
  io_reactor.should_not be_running
124
163
  end
125
164
 
126
- it 'does nothing when called before #start!' do
127
- client.shutdown!
165
+ it 'does nothing when called before #connect' do
166
+ client.close
128
167
  end
129
168
 
130
- it 'accepts multiple calls to #shutdown!' do
131
- client.start!
132
- client.shutdown!
133
- client.shutdown!
169
+ it 'accepts multiple calls to #close' do
170
+ client.connect
171
+ client.close
172
+ client.close
134
173
  end
135
174
 
136
175
  it 'returns itself' do
137
- client.start!.shutdown!.should equal(client)
176
+ client.connect.close.should equal(client)
138
177
  end
139
178
  end
140
179
 
141
180
  describe '#use' do
142
181
  before do
143
- client.start!
182
+ client.connect
144
183
  end
145
184
 
146
185
  it 'executes a USE query' do
@@ -150,12 +189,12 @@ module Cql
150
189
  end
151
190
 
152
191
  it 'executes a USE query for each connection' do
153
- client.shutdown!
192
+ client.close
154
193
  io_reactor.stop.get
155
194
  io_reactor.start.get
156
195
 
157
196
  c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
158
- c.start!
197
+ c.connect
159
198
 
160
199
  c.use('system')
161
200
  last_requests = connections.select { |c| c[:host] =~ /^h\d\.example\.com$/ }.sort_by { |c| c[:host] }.map { |c| c[:requests].last }
@@ -179,7 +218,7 @@ module Cql
179
218
 
180
219
  describe '#execute' do
181
220
  before do
182
- client.start!
221
+ client.connect
183
222
  end
184
223
 
185
224
  it 'asks the connection to execute the query' do
@@ -214,12 +253,12 @@ module Cql
214
253
  end
215
254
 
216
255
  it 'detects that one connection changed to a keyspace and changes the others too' do
217
- client.shutdown!
256
+ client.close
218
257
  io_reactor.stop.get
219
258
  io_reactor.start.get
220
259
 
221
260
  c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
222
- c.start!
261
+ c.connect
223
262
 
224
263
  io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'), connections.find { |c| c[:host] == 'h1.example.com' }[:host])
225
264
  io_reactor.queue_response(Protocol::SetKeyspaceResultResponse.new('system'), connections.find { |c| c[:host] == 'h2.example.com' }[:host])
@@ -299,7 +338,7 @@ module Cql
299
338
  end
300
339
 
301
340
  before do
302
- client.start!
341
+ client.connect
303
342
  end
304
343
 
305
344
  it 'sends a prepare request' do
@@ -334,12 +373,12 @@ module Cql
334
373
  end
335
374
 
336
375
  it 'executes a prepared statement using the right connection' do
337
- client.shutdown!
376
+ client.close
338
377
  io_reactor.stop.get
339
378
  io_reactor.start.get
340
379
 
341
380
  c = described_class.new(connection_options.merge(host: 'h1.example.com,h2.example.com,h3.example.com'))
342
- c.start!
381
+ c.connect
343
382
 
344
383
  io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, metadata))
345
384
  io_reactor.queue_response(Protocol::PreparedResultResponse.new('B' * 32, metadata))
@@ -363,51 +402,51 @@ module Cql
363
402
  end
364
403
 
365
404
  context 'when not connected' do
366
- it 'is not connected before #start! has been called' do
405
+ it 'is not connected before #connect has been called' do
367
406
  client.should_not be_connected
368
407
  end
369
408
 
370
- it 'is not connected after #shutdown! has been called' do
371
- client.start!
372
- client.shutdown!
409
+ it 'is not connected after #close has been called' do
410
+ client.connect
411
+ client.close
373
412
  client.should_not be_connected
374
413
  end
375
414
 
376
- it 'complains when #use is called before #start!' do
415
+ it 'complains when #use is called before #connect' do
377
416
  expect { client.use('system') }.to raise_error(Client::NotConnectedError)
378
417
  end
379
418
 
380
- it 'complains when #use is called after #shutdown!' do
381
- client.start!
382
- client.shutdown!
419
+ it 'complains when #use is called after #close' do
420
+ client.connect
421
+ client.close
383
422
  expect { client.use('system') }.to raise_error(Client::NotConnectedError)
384
423
  end
385
424
 
386
- it 'complains when #execute is called before #start!' do
425
+ it 'complains when #execute is called before #connect' do
387
426
  expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
388
427
  end
389
428
 
390
- it 'complains when #execute is called after #shutdown!' do
391
- client.start!
392
- client.shutdown!
429
+ it 'complains when #execute is called after #close' do
430
+ client.connect
431
+ client.close
393
432
  expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
394
433
  end
395
434
 
396
- it 'complains when #prepare is called before #start!' do
435
+ it 'complains when #prepare is called before #connect' do
397
436
  expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
398
437
  end
399
438
 
400
- it 'complains when #prepare is called after #shutdown!' do
401
- client.start!
402
- client.shutdown!
439
+ it 'complains when #prepare is called after #close' do
440
+ client.connect
441
+ client.close
403
442
  expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
404
443
  end
405
444
 
406
- it 'complains when #execute of a prepared statement is called after #shutdown!' do
407
- client.start!
445
+ it 'complains when #execute of a prepared statement is called after #close' do
446
+ client.connect
408
447
  io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, []))
409
448
  statement = client.prepare('DELETE FROM stuff WHERE id = 3')
410
- client.shutdown!
449
+ client.close
411
450
  expect { statement.execute }.to raise_error(Client::NotConnectedError)
412
451
  end
413
452
  end