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.
- data/README.md +2 -3
- data/lib/cql/client.rb +65 -15
- data/lib/cql/io.rb +2 -0
- data/lib/cql/io/io_reactor.rb +90 -304
- data/lib/cql/io/node_connection.rb +206 -0
- data/lib/cql/protocol/encoding.rb +1 -0
- data/lib/cql/protocol/request_frame.rb +26 -0
- data/lib/cql/protocol/response_frame.rb +17 -0
- data/lib/cql/version.rb +1 -1
- data/spec/cql/client_spec.rb +95 -56
- data/spec/cql/io/io_reactor_spec.rb +47 -37
- data/spec/cql/protocol/encoding_spec.rb +5 -0
- data/spec/cql/protocol/request_frame_spec.rb +74 -0
- data/spec/cql/protocol/response_frame_spec.rb +18 -0
- data/spec/integration/client_spec.rb +46 -9
- data/spec/integration/protocol_spec.rb +83 -14
- data/spec/integration/regression_spec.rb +2 -2
- data/spec/spec_helper.rb +6 -0
- metadata +3 -2
@@ -177,47 +177,61 @@ module Cql
|
|
177
177
|
Future.combine(*futures).get
|
178
178
|
end
|
179
179
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
io_reactor.
|
187
|
-
io_reactor.add_connection(host, port).on_complete do |
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
180
|
+
context 'with a connection ID' do
|
181
|
+
it 'performs the request using the specified connection' do
|
182
|
+
future = Future.new
|
183
|
+
request = Cql::Protocol::StartupRequest.new
|
184
|
+
response = "\x81\x00\x00\x02\x00\x00\x00\x00"
|
185
|
+
|
186
|
+
io_reactor.start.on_complete do
|
187
|
+
io_reactor.add_connection(host, port).on_complete do |c1_id|
|
188
|
+
io_reactor.add_connection(host, port).on_complete do |c2_id|
|
189
|
+
q1_future = io_reactor.queue_request(request, c2_id)
|
190
|
+
q2_future = io_reactor.queue_request(request, c1_id)
|
191
|
+
|
192
|
+
Future.combine(q1_future, q2_future).on_complete do |(_, q1_id), (_, q2_id)|
|
193
|
+
future.complete!([c1_id, c2_id, q1_id, q2_id])
|
194
|
+
end
|
195
|
+
|
196
|
+
server.await_connects!(2)
|
197
|
+
server.broadcast!(response.dup)
|
193
198
|
end
|
194
|
-
|
195
|
-
server.await_connects!(2)
|
196
|
-
server.broadcast!(response.dup)
|
197
199
|
end
|
198
200
|
end
|
199
|
-
end
|
200
201
|
|
201
|
-
|
202
|
+
connection1_id, connection2_id, query1_id, query2_id = future.value
|
202
203
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
204
|
+
connection1_id.should_not be_nil
|
205
|
+
connection2_id.should_not be_nil
|
206
|
+
query1_id.should == connection2_id
|
207
|
+
query2_id.should == connection1_id
|
208
|
+
end
|
208
209
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
210
|
+
it 'fails if the connection does not exist' do
|
211
|
+
f = io_reactor.start.flat_map do
|
212
|
+
io_reactor.add_connection(host, port).flat_map do
|
213
|
+
io_reactor.queue_request(Cql::Protocol::StartupRequest.new, 1234)
|
214
|
+
end
|
213
215
|
end
|
216
|
+
expect { f.get }.to raise_error(ConnectionNotFoundError)
|
214
217
|
end
|
215
|
-
expect { f.get }.to raise_error(ConnectionNotFoundError)
|
216
|
-
end
|
217
218
|
|
218
|
-
|
219
|
-
|
220
|
-
|
219
|
+
it 'fails if the connection is busy' do
|
220
|
+
f = io_reactor.start.flat_map do
|
221
|
+
io_reactor.add_connection(host, port).flat_map do
|
222
|
+
io_reactor.add_connection(host, port).flat_map do |connection_id|
|
223
|
+
200.times do
|
224
|
+
io_reactor.queue_request(Cql::Protocol::OptionsRequest.new, connection_id)
|
225
|
+
end
|
226
|
+
io_reactor.queue_request(Cql::Protocol::OptionsRequest.new, connection_id)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
expect { f.get }.to raise_error(ConnectionBusyError)
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'fails if the connection is busy, when there is only one connection' do
|
234
|
+
f = io_reactor.start.flat_map do
|
221
235
|
io_reactor.add_connection(host, port).flat_map do |connection_id|
|
222
236
|
200.times do
|
223
237
|
io_reactor.queue_request(Cql::Protocol::OptionsRequest.new, connection_id)
|
@@ -225,12 +239,8 @@ module Cql
|
|
225
239
|
io_reactor.queue_request(Cql::Protocol::OptionsRequest.new, connection_id)
|
226
240
|
end
|
227
241
|
end
|
242
|
+
expect { f.get }.to raise_error(ConnectionBusyError)
|
228
243
|
end
|
229
|
-
expect { f.get }.to raise_error(ConnectionBusyError)
|
230
|
-
end
|
231
|
-
|
232
|
-
it 'fails if the connection is busy, when there is only one connection' do
|
233
|
-
pending 'as it is the reactor doesn\'t try to deliver requests when all connections are busy'
|
234
244
|
end
|
235
245
|
|
236
246
|
it 'fails if there is an error when encoding the request' do
|
@@ -73,6 +73,11 @@ module Cql
|
|
73
73
|
buffer.should eql_bytes("\x00\x00")
|
74
74
|
end
|
75
75
|
|
76
|
+
it 'encodes a non-string' do
|
77
|
+
Encoding.write_string(buffer, 42)
|
78
|
+
buffer.should eql_bytes("\x00\x0242")
|
79
|
+
end
|
80
|
+
|
76
81
|
it 'appends to the buffer' do
|
77
82
|
buffer << "\xab"
|
78
83
|
buffer.force_encoding(::Encoding::BINARY)
|
@@ -6,6 +6,21 @@ require 'spec_helper'
|
|
6
6
|
module Cql
|
7
7
|
module Protocol
|
8
8
|
describe RequestFrame do
|
9
|
+
context 'with CREDENTIALS requests' do
|
10
|
+
it 'encodes a CREDENTIALS request' do
|
11
|
+
bytes = RequestFrame.new(CredentialsRequest.new('username' => 'cassandra', 'password' => 'ardnassac')).write('')
|
12
|
+
bytes.should == (
|
13
|
+
"\x01\x00\x00\04" +
|
14
|
+
"\x00\x00\x00\x2c" +
|
15
|
+
"\x00\x02" +
|
16
|
+
"\x00\x08username" +
|
17
|
+
"\x00\x09cassandra" +
|
18
|
+
"\x00\x08password" +
|
19
|
+
"\x00\x09ardnassac"
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
9
24
|
context 'with OPTIONS requests' do
|
10
25
|
it 'encodes an OPTIONS request' do
|
11
26
|
bytes = RequestFrame.new(OptionsRequest.new).write('')
|
@@ -150,6 +165,65 @@ module Cql
|
|
150
165
|
end
|
151
166
|
end
|
152
167
|
|
168
|
+
describe CredentialsRequest do
|
169
|
+
describe '#to_s' do
|
170
|
+
it 'returns a pretty string' do
|
171
|
+
request = CredentialsRequest.new('foo' => 'bar', 'hello' => 'world')
|
172
|
+
request.to_s.should == 'CREDENTIALS {"foo"=>"bar", "hello"=>"world"}'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe '#eql?' do
|
177
|
+
it 'returns when the credentials are the same' do
|
178
|
+
c1 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
179
|
+
c2 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
180
|
+
c2.should eql(c2)
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'returns when the credentials are equivalent' do
|
184
|
+
pending 'this would be nice, but is hardly necessary' do
|
185
|
+
c1 = CredentialsRequest.new(:username => 'foo', :password => 'bar')
|
186
|
+
c2 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
187
|
+
c1.should eql(c2)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'returns false when the credentials are different' do
|
192
|
+
c1 = CredentialsRequest.new('username' => 'foo', 'password' => 'world')
|
193
|
+
c2 = CredentialsRequest.new('username' => 'foo', 'hello' => 'world')
|
194
|
+
c1.should_not eql(c2)
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'is aliased as ==' do
|
198
|
+
c1 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
199
|
+
c2 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
200
|
+
c1.should == c2
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe '#hash' do
|
205
|
+
it 'has the same hash code as another identical object' do
|
206
|
+
c1 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
207
|
+
c2 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
208
|
+
c1.hash.should == c2.hash
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'has the same hash code as another object with equivalent credentials' do
|
212
|
+
pending 'this would be nice, but is hardly necessary' do
|
213
|
+
c1 = CredentialsRequest.new(:username => 'foo', :password => 'bar')
|
214
|
+
c2 = CredentialsRequest.new('username' => 'foo', 'password' => 'bar')
|
215
|
+
c1.hash.should == c2.hash
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'does not have the same hash code when the credentials are different' do
|
220
|
+
c1 = CredentialsRequest.new('username' => 'foo', 'password' => 'world')
|
221
|
+
c2 = CredentialsRequest.new('username' => 'foo', 'hello' => 'world')
|
222
|
+
c1.hash.should_not == c2.hash
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
153
227
|
describe OptionsRequest do
|
154
228
|
describe '#to_s' do
|
155
229
|
it 'returns a pretty string' do
|
@@ -201,6 +201,24 @@ module Cql
|
|
201
201
|
end
|
202
202
|
end
|
203
203
|
|
204
|
+
context 'when fed a complete AUTHENTICATE frame' do
|
205
|
+
before do
|
206
|
+
frame << "\x81\x00\x00\x03\x00\x00\x001\x00/org.apache.cassandra.auth.PasswordAuthenticator"
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'is complete' do
|
210
|
+
frame.should be_complete
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'has an authentication class' do
|
214
|
+
frame.body.authentication_class.should == 'org.apache.cassandra.auth.PasswordAuthenticator'
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'has a pretty #to_s representation' do
|
218
|
+
frame.body.to_s.should == 'AUTHENTICATE org.apache.cassandra.auth.PasswordAuthenticator'
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
204
222
|
context 'when fed a complete SUPPORTED frame' do
|
205
223
|
before do
|
206
224
|
frame << "\x81\x00\x00\x06\x00\x00\x00\x27"
|
@@ -5,7 +5,7 @@ require 'spec_helper'
|
|
5
5
|
|
6
6
|
describe 'A CQL client' do
|
7
7
|
let :connection_options do
|
8
|
-
{:host => ENV['CASSANDRA_HOST']}
|
8
|
+
{:host => ENV['CASSANDRA_HOST'], :credentials => {:username => 'cassandra', :password => 'cassandra'}}
|
9
9
|
end
|
10
10
|
|
11
11
|
let :client do
|
@@ -13,11 +13,11 @@ describe 'A CQL client' do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
before do
|
16
|
-
client.
|
16
|
+
client.connect
|
17
17
|
end
|
18
18
|
|
19
19
|
after do
|
20
|
-
client.
|
20
|
+
client.close
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'executes a query and returns the result' do
|
@@ -38,12 +38,12 @@ describe 'A CQL client' do
|
|
38
38
|
|
39
39
|
it 'can be initialized with a keyspace' do
|
40
40
|
c = Cql::Client.new(connection_options.merge(:keyspace => 'system'))
|
41
|
-
c.
|
41
|
+
c.connect
|
42
42
|
begin
|
43
43
|
c.keyspace.should == 'system'
|
44
44
|
expect { c.execute('SELECT * FROM schema_keyspaces') }.to_not raise_error
|
45
45
|
ensure
|
46
|
-
c.
|
46
|
+
c.close
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
@@ -68,12 +68,12 @@ describe 'A CQL client' do
|
|
68
68
|
end
|
69
69
|
|
70
70
|
before do
|
71
|
-
client.
|
72
|
-
multi_client.
|
71
|
+
client.close
|
72
|
+
multi_client.connect
|
73
73
|
end
|
74
74
|
|
75
75
|
after do
|
76
|
-
multi_client.
|
76
|
+
multi_client.close
|
77
77
|
end
|
78
78
|
|
79
79
|
it 'handles keyspace changes with #use' do
|
@@ -101,6 +101,43 @@ describe 'A CQL client' do
|
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
+
context 'with authentication' do
|
105
|
+
let :client do
|
106
|
+
stub(:client, connect: nil, close: nil)
|
107
|
+
end
|
108
|
+
|
109
|
+
let :authentication_enabled do
|
110
|
+
begin
|
111
|
+
Cql::Client.connect(connection_options.merge(credentials: nil))
|
112
|
+
false
|
113
|
+
rescue Cql::AuthenticationError
|
114
|
+
true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'send credentials given in :credentials' do
|
119
|
+
client = Cql::Client.connect(connection_options.merge(credentials: {username: 'cassandra', password: 'cassandra'}))
|
120
|
+
client.execute('SELECT * FROM system.schema_keyspaces')
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'raises an error when no credentials have been given' do
|
124
|
+
if authentication_enabled
|
125
|
+
expect { Cql::Client.connect(connection_options.merge(credentials: nil)) }.to raise_error(Cql::AuthenticationError)
|
126
|
+
else
|
127
|
+
pending 'authentication not configured'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'raises an error when the credentials are bad' do
|
132
|
+
if authentication_enabled
|
133
|
+
client = Cql::Client.new(connection_options.merge(credentials: {username: 'foo', password: 'bar'}))
|
134
|
+
expect { client.connect }.to raise_error(Cql::AuthenticationError)
|
135
|
+
else
|
136
|
+
pending 'authentication not configured'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
104
141
|
context 'with error conditions' do
|
105
142
|
it 'raises an error for CQL syntax errors' do
|
106
143
|
expect { client.execute('BAD cql') }.to raise_error(Cql::CqlError)
|
@@ -112,7 +149,7 @@ describe 'A CQL client' do
|
|
112
149
|
|
113
150
|
it 'fails gracefully when connecting to the Thrift port' do
|
114
151
|
client = Cql::Client.new(connection_options.merge(port: 9160))
|
115
|
-
expect { client.
|
152
|
+
expect { client.connect }.to raise_error(Cql::IoError)
|
116
153
|
end
|
117
154
|
end
|
118
155
|
end
|
@@ -19,10 +19,21 @@ describe 'Protocol parsing and communication' do
|
|
19
19
|
io_reactor.stop.get if io_reactor.running?
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
22
|
+
def raw_execute_request(request)
|
23
23
|
io_reactor.queue_request(request).get.first
|
24
24
|
end
|
25
25
|
|
26
|
+
def execute_request(request)
|
27
|
+
response = raw_execute_request(request)
|
28
|
+
if response.is_a?(Cql::Protocol::AuthenticateResponse)
|
29
|
+
unless response.authentication_class == 'org.apache.cassandra.auth.PasswordAuthenticator'
|
30
|
+
raise "Cassandra required an unsupported authenticator: #{response.authentication_class}"
|
31
|
+
end
|
32
|
+
response = execute_request(Cql::Protocol::CredentialsRequest.new('username' => 'cassandra', 'password' => 'cassandra'))
|
33
|
+
end
|
34
|
+
response
|
35
|
+
end
|
36
|
+
|
26
37
|
def query(cql, consistency=:one)
|
27
38
|
response = execute_request(Cql::Protocol::QueryRequest.new(cql, consistency))
|
28
39
|
raise response.to_s if response.is_a?(Cql::Protocol::ErrorResponse)
|
@@ -83,15 +94,73 @@ describe 'Protocol parsing and communication' do
|
|
83
94
|
response.options.should have_key('CQL_VERSION')
|
84
95
|
end
|
85
96
|
|
86
|
-
|
87
|
-
|
88
|
-
|
97
|
+
context 'when authentication is not required' do
|
98
|
+
it 'sends STARTUP and receives READY' do
|
99
|
+
response = execute_request(Cql::Protocol::StartupRequest.new)
|
100
|
+
response.should be_a(Cql::Protocol::ReadyResponse)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'sends a bad STARTUP and receives ERROR' do
|
104
|
+
response = execute_request(Cql::Protocol::StartupRequest.new('9.9.9'))
|
105
|
+
response.code.should == 10
|
106
|
+
response.message.should include('not supported')
|
107
|
+
end
|
89
108
|
end
|
90
109
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
110
|
+
context 'when authentication is required' do
|
111
|
+
let :authentication_enabled do
|
112
|
+
ir = Cql::Io::IoReactor.new
|
113
|
+
ir.start
|
114
|
+
connected = ir.add_connection(ENV['CASSANDRA_HOST'], 9042)
|
115
|
+
started = connected.flat_map do
|
116
|
+
ir.queue_request(Cql::Protocol::StartupRequest.new)
|
117
|
+
end
|
118
|
+
response = started.get.first
|
119
|
+
required = response.is_a?(Cql::Protocol::AuthenticateResponse)
|
120
|
+
ir.stop.get
|
121
|
+
required
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'sends STARTUP and receives AUTHENTICATE' do
|
125
|
+
if authentication_enabled
|
126
|
+
response = raw_execute_request(Cql::Protocol::StartupRequest.new)
|
127
|
+
response.should be_a(Cql::Protocol::AuthenticateResponse)
|
128
|
+
else
|
129
|
+
pending 'authentication not configured'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'ignores the AUTHENTICATE response and receives ERROR' do
|
134
|
+
if authentication_enabled
|
135
|
+
raw_execute_request(Cql::Protocol::StartupRequest.new)
|
136
|
+
response = raw_execute_request(Cql::Protocol::RegisterRequest.new('TOPOLOGY_CHANGE'))
|
137
|
+
response.code.should == 10
|
138
|
+
response.message.should include('needs authentication')
|
139
|
+
else
|
140
|
+
pending 'authentication not configured'
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'sends STARTUP followed by CREDENTIALS and receives READY' do
|
145
|
+
if authentication_enabled
|
146
|
+
raw_execute_request(Cql::Protocol::StartupRequest.new)
|
147
|
+
response = raw_execute_request(Cql::Protocol::CredentialsRequest.new('username' => 'cassandra', 'password' => 'cassandra'))
|
148
|
+
response.should be_a(Cql::Protocol::ReadyResponse)
|
149
|
+
else
|
150
|
+
pending 'authentication not configured'
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'sends bad username and password in CREDENTIALS and receives ERROR' do
|
155
|
+
if authentication_enabled
|
156
|
+
raw_execute_request(Cql::Protocol::StartupRequest.new)
|
157
|
+
response = raw_execute_request(Cql::Protocol::CredentialsRequest.new('username' => 'foo', 'password' => 'bar'))
|
158
|
+
response.code.should == 0x100
|
159
|
+
response.message.should include('Username and/or password are incorrect')
|
160
|
+
else
|
161
|
+
pending 'authentication not configured'
|
162
|
+
end
|
163
|
+
end
|
95
164
|
end
|
96
165
|
end
|
97
166
|
|
@@ -223,12 +292,12 @@ describe 'Protocol parsing and communication' do
|
|
223
292
|
end
|
224
293
|
end
|
225
294
|
|
226
|
-
it 'sends a TRUNCATE command' do
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
end
|
295
|
+
# it 'sends a TRUNCATE command' do
|
296
|
+
# in_keyspace_with_table do
|
297
|
+
# response = query(%<TRUNCATE users>)
|
298
|
+
# response.should be_void
|
299
|
+
# end
|
300
|
+
# end
|
232
301
|
|
233
302
|
it 'sends a BATCH command' do
|
234
303
|
in_keyspace_with_table do
|