cql-rb 1.0.0.pre4 → 1.0.0.pre5

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