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.
- 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
|