cql-rb 1.0.0.pre0 → 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +139 -3
- data/lib/cql/client.rb +144 -15
- data/lib/cql/future.rb +75 -0
- data/lib/cql/io.rb +2 -1
- data/lib/cql/io/io_reactor.rb +57 -4
- data/lib/cql/protocol.rb +3 -1
- data/lib/cql/uuid.rb +22 -0
- data/lib/cql/version.rb +1 -1
- data/spec/cql/client_spec.rb +43 -9
- data/spec/cql/io/io_reactor_spec.rb +25 -29
- data/spec/cql/uuid_spec.rb +16 -0
- data/spec/integration/client_spec.rb +10 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/await_helper.rb +20 -0
- data/spec/support/fake_server.rb +6 -3
- metadata +4 -2
data/README.md
CHANGED
@@ -1,10 +1,146 @@
|
|
1
|
-
# Ruby
|
1
|
+
# Ruby CQL3 driver
|
2
2
|
|
3
|
-
|
3
|
+
_There has not yet been a stable release of this project._
|
4
|
+
|
5
|
+
|
6
|
+
# Requirements
|
7
|
+
|
8
|
+
Cassandra 1.2 with the native transport protocol turned on and a modern Ruby. Tested with Ruby 1.9.3 and JRuby and 1.7.x.
|
9
|
+
|
10
|
+
# Installation
|
11
|
+
|
12
|
+
gem install --prerelease cql-rb
|
13
|
+
|
14
|
+
## Configure Cassandra
|
15
|
+
|
16
|
+
The native transport protocol (sometimes called binary protocol, or CQL protocol) is not on by default in Cassandra 1.2, to enable it edit the `cassandra.yaml` file on all nodes in your cluster and set `start_native_transport` to `true`. You need to restart the nodes for this to have effect.
|
17
|
+
|
18
|
+
# Quick start
|
19
|
+
|
20
|
+
require 'cql'
|
21
|
+
|
22
|
+
client = Cql::Client.new(host: 'cassandra.example.com')
|
23
|
+
client.start!
|
24
|
+
client.use('system')
|
25
|
+
rows = client.execute('SELECT keyspace_name, columnfamily_name FROM schema_columnfamilies')
|
26
|
+
rows.each do |row|
|
27
|
+
puts "The keyspace #{row['keyspace_name']} has a table called #{row['columnfamily_name']}""
|
28
|
+
end
|
29
|
+
|
30
|
+
when you're done you can call `#shutdown!` to disconnect from Cassandra. You can connect to multiple Cassandra nodes by passing multiple comma separated host names to the `:host` option.
|
31
|
+
|
32
|
+
## Changing keyspaces
|
33
|
+
|
34
|
+
client.use('measurements')
|
35
|
+
|
36
|
+
or using CQL:
|
37
|
+
|
38
|
+
client.execute('USE measurements')
|
39
|
+
|
40
|
+
## Running queries
|
41
|
+
|
42
|
+
You run CQL statements by passing them to `#execute`. Most statements don't have any result and the call will return nil.
|
43
|
+
|
44
|
+
client.execute("INSERT INTO events (id, date, description) VALUES (23462, '2013-02-24T10:14:23+0000', 'Rang bell, ate food')")
|
45
|
+
|
46
|
+
client.execute("UPDATE events SET description = 'Oh, my' WHERE id = 13126")
|
47
|
+
|
48
|
+
|
49
|
+
If the CQL statement passed to `#execute` returns a result (e.g. it's a `SELECT` statement) the call returns an enumerable of rows:
|
50
|
+
|
51
|
+
rows = client.execute('SELECT date, description FROM events')
|
52
|
+
rows.each do |row|
|
53
|
+
row.each do |key, value|
|
54
|
+
puts "#{key} = #{value}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
The enumerable also has an accessor called `metadata` which returns a description of the rows and columns:
|
59
|
+
|
60
|
+
rows = client.execute('SELECT date, description FROM events'
|
61
|
+
rows.metadata['date'].type # => :date
|
62
|
+
|
63
|
+
## Creating keyspaces and tables
|
64
|
+
|
65
|
+
There is no special facility for creating keyspaces and tables, they are created by executing CQL:
|
66
|
+
|
67
|
+
keyspace_definition = <<-KSDEF
|
68
|
+
CREATE KEYSPACE measurements
|
69
|
+
WITH replication = {
|
70
|
+
'class': 'SimpleStrategy',
|
71
|
+
'replication_factor': 3
|
72
|
+
}
|
73
|
+
KSDEF
|
74
|
+
|
75
|
+
table_definition = <<- TABLEDEF
|
76
|
+
CREATE TABLE events (
|
77
|
+
id INT,
|
78
|
+
date DATE,
|
79
|
+
comment VARCHAR,
|
80
|
+
PRIMARY KEY (id)
|
81
|
+
)
|
82
|
+
TABLEDEF
|
83
|
+
|
84
|
+
client.execute(keyspace_definition)
|
85
|
+
client.use(measurements)
|
86
|
+
client.execute(table_definition)
|
87
|
+
|
88
|
+
You can also `ALTER` keyspaces and tables.
|
89
|
+
|
90
|
+
## Prepared statements
|
91
|
+
|
92
|
+
The driver supports prepared statements. Use `#prepare` to create a statement object, and then call `#execute` on that object to run a statement. You must supply values for all bound parameters when you call `#execute`.
|
93
|
+
|
94
|
+
statement = client.prepare('SELECT date, description FROM events WHERE id = ?')
|
95
|
+
rows = statement.execute(1235)
|
96
|
+
|
97
|
+
A prepared statement can be run many times, but the CQL parsing will only be done once. Use prepared statements for queries you run over and over again.
|
98
|
+
|
99
|
+
`INSERT`, `UPDATE`, `DELETE` and `SELECT` statements can be prepared, other statements may raise `QueryError`.
|
100
|
+
|
101
|
+
At this time prepared statements are local to a single connection. Even if you connect to multiple nodes a prepared statement is only ever going to be executed against one of the nodes.
|
102
|
+
|
103
|
+
# Consistency levels
|
104
|
+
|
105
|
+
The `#execute` method supports setting the desired consistency level for the statement:
|
106
|
+
|
107
|
+
client.execute('SELECT * FROM peers', :local_quorum)
|
108
|
+
|
109
|
+
The possible values are:
|
110
|
+
|
111
|
+
* `:any`
|
112
|
+
* `:one`
|
113
|
+
* `:two`
|
114
|
+
* `:three`
|
115
|
+
* `:quorum`
|
116
|
+
* `:all`
|
117
|
+
* `:local_quorum`
|
118
|
+
* `:each_quorum`
|
119
|
+
|
120
|
+
The default consistency level is `:quorum`.
|
121
|
+
|
122
|
+
Consistency level is ignored for `USE`, `TRUNCATE`, `CREATE` and `ALTER` statements, and some (like `:any`) aren't allowed in all situations.
|
123
|
+
|
124
|
+
## CQL3
|
125
|
+
|
126
|
+
This is just a driver for the Cassandra native CQL protocol, it doesn't really know anything about CQL. You can run any CQL3 statement and the driver will return whatever Cassandra replies with.
|
127
|
+
|
128
|
+
Read more about CQL3 in the [CQL3 syntax documentation](https://github.com/apache/cassandra/blob/cassandra-1.2/doc/cql3/CQL.textile) and the [Cassandra query documentation](http://www.datastax.com/docs/1.2/cql_cli/querying_cql).
|
129
|
+
|
130
|
+
# Known bugs & limitations
|
131
|
+
|
132
|
+
* If any connection raises an error the whole IO reactor shuts down.
|
133
|
+
* JRuby 1.6.8 is not supported, although it should be. The only known issue is that connection failures aren't handled gracefully.
|
134
|
+
* No automatic peer discovery.
|
135
|
+
* You can't specify consistency level when executing prepared statements.
|
136
|
+
* Authentication is not supported.
|
137
|
+
* Compression is not supported.
|
138
|
+
* Large results are buffered in memory until the whole response has been loaded, the protocol makes it possible to start to deliver rows to the client code as soon as the metadata is loaded, but this is not supported yet.
|
139
|
+
* There is no cluster introspection utilities (like the `DESCRIBE` commands in `cqlsh`).
|
4
140
|
|
5
141
|
## Copyright
|
6
142
|
|
7
|
-
Copyright 2013 Theo Hultberg
|
143
|
+
Copyright 2013 Theo Hultberg/Iconara
|
8
144
|
|
9
145
|
_Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License You may obtain a copy of the License at_
|
10
146
|
|
data/lib/cql/client.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Cql
|
4
|
-
NotConnectedError = Class.new(CqlError)
|
5
|
-
InvalidKeyspaceNameError = Class.new(CqlError)
|
6
|
-
|
7
4
|
class QueryError < CqlError
|
8
5
|
attr_reader :code
|
9
6
|
|
@@ -13,7 +10,54 @@ module Cql
|
|
13
10
|
end
|
14
11
|
end
|
15
12
|
|
13
|
+
ClientError = Class.new(CqlError)
|
14
|
+
|
15
|
+
# A CQL client manages connections to one or more Cassandra nodes and you use
|
16
|
+
# it run queries, insert and update data.
|
17
|
+
#
|
18
|
+
# @example Connecting and changing to a keyspace
|
19
|
+
# # create a client that will connect to two Cassandra nodes
|
20
|
+
# client = Cql::Client.new(host: 'node01.cassandra.local,node02.cassandra.local')
|
21
|
+
# # establish node connections
|
22
|
+
# client.start!
|
23
|
+
# # change to a keyspace
|
24
|
+
# client.use('stuff')
|
25
|
+
#
|
26
|
+
# @example Query for data
|
27
|
+
# rows = client.execute('SELECT * FROM things WHERE id = 2')
|
28
|
+
# rows.each do |row|
|
29
|
+
# p row
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @example Inserting and updating data
|
33
|
+
# client.execute("INSERT INTO things (id, value) VALUES (4, 'foo')")
|
34
|
+
# client.execute("UPDATE things SET value = 'bar' WHERE id = 5")
|
35
|
+
#
|
36
|
+
# @example Prepared statements
|
37
|
+
# statement = client.prepare('INSERT INTO things (id, value) VALUES (?, ?)')
|
38
|
+
# statement.execute(9, 'qux')
|
39
|
+
# statement.execute(8, 'baz')
|
40
|
+
#
|
41
|
+
# Client instances are threadsafe.
|
42
|
+
#
|
16
43
|
class Client
|
44
|
+
NotConnectedError = Class.new(ClientError)
|
45
|
+
InvalidKeyspaceNameError = Class.new(ClientError)
|
46
|
+
|
47
|
+
# Create a new client.
|
48
|
+
#
|
49
|
+
# Creating a client does not automatically connect to Cassandra, you need to
|
50
|
+
# call {#start!} to connect. `#start!` returns `self` so you can chain that
|
51
|
+
# call after `new`.
|
52
|
+
#
|
53
|
+
# @param [Hash] options
|
54
|
+
# @option options [String] :host ('localhost') One or more (comma separated)
|
55
|
+
# hostnames for the Cassandra nodes you want to connect to.
|
56
|
+
# @option options [String] :port (9042) The port to connect to
|
57
|
+
# @option options [Integer] :connection_timeout (5) Max time to wait for a
|
58
|
+
# connection, in seconds
|
59
|
+
# @option options [String] :keyspace The keyspace to change to immediately
|
60
|
+
# after all connections have been established, this is optional.
|
17
61
|
def initialize(options={})
|
18
62
|
connection_timeout = options[:connection_timeout]
|
19
63
|
@host = options[:host] || 'localhost'
|
@@ -26,24 +70,44 @@ module Cql
|
|
26
70
|
@connection_keyspaces = {}
|
27
71
|
end
|
28
72
|
|
73
|
+
# Connect to all nodes.
|
74
|
+
#
|
75
|
+
# You must call this method before you call any of the other methods of a
|
76
|
+
# client. Calling it again will have no effect.
|
77
|
+
#
|
78
|
+
# If `:keyspace` was specified when the client was created the current
|
79
|
+
# keyspace will also be changed (otherwise the current keyspace will not
|
80
|
+
# be set).
|
81
|
+
#
|
82
|
+
# @return self
|
83
|
+
#
|
29
84
|
def start!
|
30
85
|
@lock.synchronize do
|
31
86
|
return if @started
|
32
87
|
@started = true
|
33
88
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
89
|
+
begin
|
90
|
+
@io_reactor.start
|
91
|
+
hosts = @host.split(',')
|
92
|
+
start_request = Protocol::StartupRequest.new
|
93
|
+
connection_futures = hosts.map do |host|
|
94
|
+
@io_reactor.add_connection(host, @port).flat_map do |connection_id|
|
95
|
+
execute_request(start_request, connection_id).map { connection_id }
|
96
|
+
end
|
40
97
|
end
|
98
|
+
@connection_ids = Future.combine(*connection_futures).get
|
99
|
+
use(@initial_keyspace) if @initial_keyspace
|
100
|
+
rescue
|
101
|
+
@started = false
|
102
|
+
raise
|
41
103
|
end
|
42
|
-
@connection_ids = Future.combine(*connection_futures).get
|
43
|
-
use(@initial_keyspace) if @initial_keyspace
|
44
104
|
self
|
45
105
|
end
|
46
106
|
|
107
|
+
# Disconnect from all nodes.
|
108
|
+
#
|
109
|
+
# @return self
|
110
|
+
#
|
47
111
|
def shutdown!
|
48
112
|
@lock.synchronize do
|
49
113
|
return if @shut_down
|
@@ -54,12 +118,27 @@ module Cql
|
|
54
118
|
self
|
55
119
|
end
|
56
120
|
|
121
|
+
# Returns whether or not the client is connected.
|
122
|
+
#
|
123
|
+
def connected?
|
124
|
+
@started
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns the name of the current keyspace, or `nil` if no keyspace has been
|
128
|
+
# set yet.
|
129
|
+
#
|
57
130
|
def keyspace
|
58
131
|
@lock.synchronize do
|
59
132
|
return @connection_ids.map { |id| @connection_keyspaces[id] }.first
|
60
133
|
end
|
61
134
|
end
|
62
135
|
|
136
|
+
# Changes keyspace by sending a `USE` statement to all connections.
|
137
|
+
#
|
138
|
+
# The the second parameter is meant for internal use only.
|
139
|
+
#
|
140
|
+
# @raise [Cql::NotConnectedError] raised when the client is not connected
|
141
|
+
#
|
63
142
|
def use(keyspace, connection_ids=@connection_ids)
|
64
143
|
raise NotConnectedError unless @started
|
65
144
|
if check_keyspace_name!(keyspace)
|
@@ -77,16 +156,32 @@ module Cql
|
|
77
156
|
end
|
78
157
|
end
|
79
158
|
|
159
|
+
# Execute a CQL statement
|
160
|
+
#
|
161
|
+
# @raise [Cql::NotConnectedError] raised when the client is not connected
|
162
|
+
# @raise [Cql::QueryError] raised when the CQL has syntax errors or for
|
163
|
+
# other situations when the server complains.
|
164
|
+
# @return [nil, Enumerable<Hash>] Most statements have no result and return
|
165
|
+
# `nil`, but `SELECT` statements return an `Enumerable` of rows
|
166
|
+
# (see {QueryResult}).
|
167
|
+
#
|
80
168
|
def execute(cql, consistency=:quorum)
|
81
169
|
result = execute_request(Protocol::QueryRequest.new(cql, consistency)).value
|
82
170
|
ensure_keyspace!
|
83
171
|
result
|
84
172
|
end
|
85
173
|
|
174
|
+
# @private
|
86
175
|
def execute_statement(connection_id, statement_id, metadata, values, consistency)
|
87
176
|
execute_request(Protocol::ExecuteRequest.new(statement_id, metadata, values, consistency), connection_id).value
|
88
177
|
end
|
89
178
|
|
179
|
+
# Returns a prepared statement that can be run over and over again with
|
180
|
+
# different values.
|
181
|
+
#
|
182
|
+
# @raise [Cql::NotConnectedError] raised when the client is not connected
|
183
|
+
# @return [Cql::PreparedStatement] an object encapsulating the prepared statement
|
184
|
+
#
|
90
185
|
def prepare(cql)
|
91
186
|
execute_request(Protocol::PrepareRequest.new(cql)).value
|
92
187
|
end
|
@@ -136,58 +231,92 @@ module Cql
|
|
136
231
|
use(ks, @connection_ids) if ks
|
137
232
|
end
|
138
233
|
|
234
|
+
public
|
235
|
+
|
236
|
+
#
|
139
237
|
class PreparedStatement
|
238
|
+
# @return [ResultMetadata]
|
239
|
+
attr_reader :metadata
|
240
|
+
|
140
241
|
def initialize(*args)
|
141
|
-
@client, @connection_id, @statement_id, @
|
242
|
+
@client, @connection_id, @statement_id, @raw_metadata = args
|
243
|
+
@metadata = ResultMetadata.new(@raw_metadata)
|
142
244
|
end
|
143
245
|
|
246
|
+
# Execute the prepared statement with a list of values for the bound parameters.
|
247
|
+
#
|
144
248
|
def execute(*args)
|
145
|
-
@client.execute_statement(@connection_id, @statement_id, @
|
249
|
+
@client.execute_statement(@connection_id, @statement_id, @raw_metadata, args, :quorum)
|
146
250
|
end
|
147
251
|
end
|
148
252
|
|
149
253
|
class QueryResult
|
150
254
|
include Enumerable
|
151
255
|
|
256
|
+
# @return [ResultMetadata]
|
152
257
|
attr_reader :metadata
|
153
258
|
|
259
|
+
# @private
|
154
260
|
def initialize(metadata, rows)
|
155
261
|
@metadata = ResultMetadata.new(metadata)
|
156
262
|
@rows = rows
|
157
263
|
end
|
158
264
|
|
265
|
+
# Returns whether or not there are any rows in this result set
|
266
|
+
#
|
159
267
|
def empty?
|
160
268
|
@rows.empty?
|
161
269
|
end
|
162
270
|
|
271
|
+
# Iterates over each row in the result set.
|
272
|
+
#
|
273
|
+
# @yieldparam [Hash] row each row in the result set as a hash
|
274
|
+
# @return [Enumerable<Hash>]
|
275
|
+
#
|
163
276
|
def each(&block)
|
164
277
|
@rows.each(&block)
|
165
278
|
end
|
279
|
+
alias_method :each_row, :each
|
166
280
|
end
|
167
281
|
|
168
282
|
class ResultMetadata
|
169
283
|
include Enumerable
|
170
284
|
|
285
|
+
# @private
|
171
286
|
def initialize(metadata)
|
172
|
-
@metadata =
|
287
|
+
@metadata = metadata.each_with_object({}) { |m, h| h[m[2]] = ColumnMetadata.new(*m) }
|
173
288
|
end
|
174
289
|
|
290
|
+
# Returns the column metadata
|
291
|
+
#
|
292
|
+
# @return [ColumnMetadata] column_metadata the metadata for the column
|
293
|
+
#
|
175
294
|
def [](column_name)
|
176
295
|
@metadata[column_name]
|
177
296
|
end
|
178
297
|
|
298
|
+
# Iterates over the metadata for each column
|
299
|
+
#
|
300
|
+
# @yieldparam [ColumnMetadata] metadata the metadata for each column
|
301
|
+
# @return [Enumerable<ColumnMetadata>]
|
302
|
+
#
|
179
303
|
def each(&block)
|
180
304
|
@metadata.each_value(&block)
|
181
305
|
end
|
182
306
|
end
|
183
307
|
|
308
|
+
# Represents metadata about a column in a query result set or prepared
|
309
|
+
# statement. Apart from the keyspace, table and column names there's also
|
310
|
+
# the type as a symbol (e.g. `:varchar`, `:int`, `:date`).
|
184
311
|
class ColumnMetadata
|
185
|
-
attr_reader :keyspace, :table, :
|
312
|
+
attr_reader :keyspace, :table, :column_name, :type
|
186
313
|
|
314
|
+
# @private
|
187
315
|
def initialize(*args)
|
188
316
|
@keyspace, @table, @column_name, @type = args
|
189
317
|
end
|
190
318
|
|
319
|
+
# @private
|
191
320
|
def to_ary
|
192
321
|
[@keyspace, @table, @column_name, @type]
|
193
322
|
end
|
data/lib/cql/future.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
module Cql
|
4
4
|
FutureError = Class.new(CqlError)
|
5
5
|
|
6
|
+
# A future represents the value of a process that may not yet have completed.
|
7
|
+
#
|
6
8
|
class Future
|
7
9
|
def initialize
|
8
10
|
@complete_listeners = []
|
@@ -11,18 +13,43 @@ module Cql
|
|
11
13
|
@state_lock = Mutex.new
|
12
14
|
end
|
13
15
|
|
16
|
+
# Combine multiple futures into a new future which completes when all
|
17
|
+
# constituent futures complete, or fail when one or more of them fails.
|
18
|
+
#
|
19
|
+
# The value of the combined future is an array of the values of the
|
20
|
+
# constituent futures.
|
21
|
+
#
|
22
|
+
# @param [Array<Future>] futures the futures to combine
|
23
|
+
# @return [Future<Array>] an array of the values of the constituent futures
|
24
|
+
#
|
14
25
|
def self.combine(*futures)
|
15
26
|
CombinedFuture.new(*futures)
|
16
27
|
end
|
17
28
|
|
29
|
+
# Creates a new future which is completed.
|
30
|
+
#
|
31
|
+
# @param [Object, nil] value the value of the created future
|
32
|
+
# @return [Future] a completed future
|
33
|
+
#
|
18
34
|
def self.completed(value=nil)
|
19
35
|
CompletedFuture.new(value)
|
20
36
|
end
|
21
37
|
|
38
|
+
# Creates a new future which is failed.
|
39
|
+
#
|
40
|
+
# @param [Error] the error of the created future
|
41
|
+
# @return [Future] a failed future
|
42
|
+
#
|
22
43
|
def self.failed(error)
|
23
44
|
FailedFuture.new(error)
|
24
45
|
end
|
25
46
|
|
47
|
+
# Completes the future.
|
48
|
+
#
|
49
|
+
# This will trigger all completion listeners in the calling thread.
|
50
|
+
#
|
51
|
+
# @param [Object] v the value of the future
|
52
|
+
#
|
26
53
|
def complete!(v=nil)
|
27
54
|
@state_lock.synchronize do
|
28
55
|
raise FutureError, 'Future already completed' if complete? || failed?
|
@@ -37,10 +64,16 @@ module Cql
|
|
37
64
|
end
|
38
65
|
end
|
39
66
|
|
67
|
+
# Returns whether or not the future is complete
|
68
|
+
#
|
40
69
|
def complete?
|
41
70
|
defined? @value
|
42
71
|
end
|
43
72
|
|
73
|
+
# Registers a listener for when this future completes
|
74
|
+
#
|
75
|
+
# @yieldparam [Object] value the value of the completed future
|
76
|
+
#
|
44
77
|
def on_complete(&listener)
|
45
78
|
@state_lock.synchronize do
|
46
79
|
if complete?
|
@@ -51,6 +84,12 @@ module Cql
|
|
51
84
|
end
|
52
85
|
end
|
53
86
|
|
87
|
+
# Returns the value of this future, blocking until it is available, if necessary.
|
88
|
+
#
|
89
|
+
# If the future fails this method will raise the error that failed the future.
|
90
|
+
#
|
91
|
+
# @returns [Object] the value of this future
|
92
|
+
#
|
54
93
|
def value
|
55
94
|
raise @error if @error
|
56
95
|
return @value if defined? @value
|
@@ -60,6 +99,12 @@ module Cql
|
|
60
99
|
end
|
61
100
|
alias_method :get, :value
|
62
101
|
|
102
|
+
# Fails the future.
|
103
|
+
#
|
104
|
+
# This will trigger all failure listeners in the calling thread.
|
105
|
+
#
|
106
|
+
# @param [Error] error the error which prevented the value from being determined
|
107
|
+
#
|
63
108
|
def fail!(error)
|
64
109
|
@state_lock.synchronize do
|
65
110
|
raise FutureError, 'Future already completed' if failed? || complete?
|
@@ -74,10 +119,16 @@ module Cql
|
|
74
119
|
end
|
75
120
|
end
|
76
121
|
|
122
|
+
# Returns whether or not the future is failed.
|
123
|
+
#
|
77
124
|
def failed?
|
78
125
|
!!@error
|
79
126
|
end
|
80
127
|
|
128
|
+
# Registers a listener for when this future fails
|
129
|
+
#
|
130
|
+
# @yieldparam [Error] error the error that failed the future
|
131
|
+
#
|
81
132
|
def on_failure(&listener)
|
82
133
|
@state_lock.synchronize do
|
83
134
|
if failed?
|
@@ -88,6 +139,15 @@ module Cql
|
|
88
139
|
end
|
89
140
|
end
|
90
141
|
|
142
|
+
# Returns a new future representing a transformation of this future's value.
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# future2 = future1.map { |value| value * 2 }
|
146
|
+
#
|
147
|
+
# @yieldparam [Object] value the value of this future
|
148
|
+
# @yieldreturn [Object] the transformed value
|
149
|
+
# @return [Future] a new future representing the transformed value
|
150
|
+
#
|
91
151
|
def map(&block)
|
92
152
|
fp = Future.new
|
93
153
|
on_failure { |e| fp.fail!(e) }
|
@@ -102,6 +162,18 @@ module Cql
|
|
102
162
|
fp
|
103
163
|
end
|
104
164
|
|
165
|
+
# Returns a new future representing a transformation of this future's value,
|
166
|
+
# but where the transformation itself may be asynchronous.
|
167
|
+
#
|
168
|
+
# @example
|
169
|
+
# future2 = future1.flat_map { |value| method_returning_a_future(value) }
|
170
|
+
#
|
171
|
+
# This method is useful when you want to chain asynchronous operations.
|
172
|
+
#
|
173
|
+
# @yieldparam [Object] value the value of this future
|
174
|
+
# @yieldreturn [Future] a future representing the transformed value
|
175
|
+
# @return [Future] a new future representing the transformed value
|
176
|
+
#
|
105
177
|
def flat_map(&block)
|
106
178
|
fp = Future.new
|
107
179
|
on_failure { |e| fp.fail!(e) }
|
@@ -120,6 +192,7 @@ module Cql
|
|
120
192
|
end
|
121
193
|
end
|
122
194
|
|
195
|
+
# @private
|
123
196
|
class CompletedFuture < Future
|
124
197
|
def initialize(value=nil)
|
125
198
|
super()
|
@@ -127,6 +200,7 @@ module Cql
|
|
127
200
|
end
|
128
201
|
end
|
129
202
|
|
203
|
+
# @private
|
130
204
|
class FailedFuture < Future
|
131
205
|
def initialize(error)
|
132
206
|
super()
|
@@ -134,6 +208,7 @@ module Cql
|
|
134
208
|
end
|
135
209
|
end
|
136
210
|
|
211
|
+
# @private
|
137
212
|
class CombinedFuture < Future
|
138
213
|
def initialize(*futures)
|
139
214
|
super()
|
data/lib/cql/io.rb
CHANGED
data/lib/cql/io/io_reactor.rb
CHANGED
@@ -6,7 +6,17 @@ require 'resolv-replace'
|
|
6
6
|
|
7
7
|
module Cql
|
8
8
|
module Io
|
9
|
+
# An instance of IO reactor manages the connections used by a client.
|
10
|
+
#
|
11
|
+
# The reactor starts a thread in which all IO is performed. The IO reactor
|
12
|
+
# instances are thread safe.
|
13
|
+
#
|
9
14
|
class IoReactor
|
15
|
+
#
|
16
|
+
# @param [Hash] options
|
17
|
+
# @option options [Integer] :connection_timeout (5) Max time to wait for a
|
18
|
+
# connection, in seconds
|
19
|
+
#
|
10
20
|
def initialize(options={})
|
11
21
|
@connection_timeout = options[:connection_timeout] || 5
|
12
22
|
@lock = Mutex.new
|
@@ -18,10 +28,19 @@ module Cql
|
|
18
28
|
@running = false
|
19
29
|
end
|
20
30
|
|
31
|
+
# Returns whether or not the reactor is running
|
32
|
+
#
|
21
33
|
def running?
|
22
34
|
@running
|
23
35
|
end
|
24
36
|
|
37
|
+
# Starts the reactor.
|
38
|
+
#
|
39
|
+
# Calling this method when the reactor is connecting or is connected has
|
40
|
+
# no effect.
|
41
|
+
#
|
42
|
+
# @return [Future<nil>] a future which completes when the reactor has started
|
43
|
+
#
|
25
44
|
def start
|
26
45
|
@lock.synchronize do
|
27
46
|
unless @running
|
@@ -42,11 +61,25 @@ module Cql
|
|
42
61
|
@started_future
|
43
62
|
end
|
44
63
|
|
64
|
+
# Stops the reactor.
|
65
|
+
#
|
66
|
+
# Calling this method when the reactor is stopping or has stopped has
|
67
|
+
# no effect.
|
68
|
+
#
|
69
|
+
# @return [Future<nil>] a future which completes when the reactor has stopped
|
70
|
+
#
|
45
71
|
def stop
|
46
72
|
@running = false
|
47
73
|
@stopped_future
|
48
74
|
end
|
49
75
|
|
76
|
+
# Establish a new connection.
|
77
|
+
#
|
78
|
+
# @param [String] host The hostname to connect to
|
79
|
+
# @param [Integer] port The port to connect to
|
80
|
+
# @return [Future<Object>] a future representing the ID of the newly
|
81
|
+
# established connection, or connection error if the connection fails.
|
82
|
+
#
|
50
83
|
def add_connection(host, port)
|
51
84
|
connection = NodeConnection.new(host, port, @connection_timeout)
|
52
85
|
future = connection.open
|
@@ -65,12 +98,23 @@ module Cql
|
|
65
98
|
future
|
66
99
|
end
|
67
100
|
|
101
|
+
# Sends a request over a random, or specific connection.
|
102
|
+
#
|
103
|
+
# @param [Cql::Protocol::RequestBody] request the request to send
|
104
|
+
# @param [Object] connection_id the ID of the connection which should be
|
105
|
+
# used to send the request
|
106
|
+
# @return [Future<ResultResponse>] a future representing the result of the request
|
107
|
+
#
|
68
108
|
def queue_request(request, connection_id=nil)
|
69
109
|
future = Future.new
|
70
110
|
command_queue_push(:request, request, future, connection_id)
|
71
111
|
future
|
72
112
|
end
|
73
113
|
|
114
|
+
# Registers a listener to receive server sent events.
|
115
|
+
#
|
116
|
+
# @yieldparam [Cql::Protocol::EventResponse] event the event sent by the server
|
117
|
+
#
|
74
118
|
def add_event_listener(&listener)
|
75
119
|
command_queue_push(:event_listener, listener)
|
76
120
|
end
|
@@ -108,6 +152,7 @@ module Cql
|
|
108
152
|
end
|
109
153
|
end
|
110
154
|
|
155
|
+
# @private
|
111
156
|
class NodeConnection
|
112
157
|
def initialize(*args)
|
113
158
|
@host, @port, @connection_timeout = args
|
@@ -171,6 +216,9 @@ module Cql
|
|
171
216
|
stream_id = next_stream_id
|
172
217
|
Protocol::RequestFrame.new(request, stream_id).write(@write_buffer)
|
173
218
|
@response_tasks[stream_id] = future
|
219
|
+
rescue => e
|
220
|
+
@response_tasks.delete(stream_id)
|
221
|
+
future.fail!(e)
|
174
222
|
end
|
175
223
|
|
176
224
|
def handle_read
|
@@ -201,7 +249,12 @@ module Cql
|
|
201
249
|
|
202
250
|
def close
|
203
251
|
if @io
|
204
|
-
|
252
|
+
begin
|
253
|
+
@io.close
|
254
|
+
rescue SystemCallError
|
255
|
+
# nothing to do, it wasn't open
|
256
|
+
end
|
257
|
+
@io = nil
|
205
258
|
if connecting?
|
206
259
|
succeed_connection!
|
207
260
|
end
|
@@ -223,7 +276,7 @@ module Cql
|
|
223
276
|
EVENT_STREAM_ID = -1
|
224
277
|
|
225
278
|
def connecting?
|
226
|
-
|
279
|
+
!(@connected_future.complete? || @connected_future.failed?)
|
227
280
|
end
|
228
281
|
|
229
282
|
def handle_connected
|
@@ -246,8 +299,7 @@ module Cql
|
|
246
299
|
error = ConnectionError.new(message)
|
247
300
|
error.set_backtrace(e.backtrace) if e
|
248
301
|
@connected_future.fail!(error)
|
249
|
-
|
250
|
-
@io = nil
|
302
|
+
close
|
251
303
|
end
|
252
304
|
|
253
305
|
def next_stream_id
|
@@ -258,6 +310,7 @@ module Cql
|
|
258
310
|
end
|
259
311
|
end
|
260
312
|
|
313
|
+
# @private
|
261
314
|
class CommandDispatcher
|
262
315
|
def initialize(*args)
|
263
316
|
@io, @command_queue, @queue_lock, @node_connections = args
|
data/lib/cql/protocol.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Cql
|
4
|
+
ProtocolError = Class.new(CqlError)
|
5
|
+
|
6
|
+
# @private
|
4
7
|
module Protocol
|
5
|
-
ProtocolError = Class.new(CqlError)
|
6
8
|
DecodingError = Class.new(ProtocolError)
|
7
9
|
EncodingError = Class.new(ProtocolError)
|
8
10
|
InvalidStreamIdError = Class.new(ProtocolError)
|
data/lib/cql/uuid.rb
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Cql
|
4
|
+
# Represents a UUID value.
|
5
|
+
#
|
6
|
+
# This is a very basic implementation of UUIDs and exists more or less just
|
7
|
+
# to encode and decode UUIDs from and to Cassandra.
|
8
|
+
#
|
4
9
|
class Uuid
|
10
|
+
# Creates a new UUID either from a string (expected to be on the standard
|
11
|
+
# 8-4-4-4-12 form, or just 32 characters without hyphens), or from a
|
12
|
+
# 128 bit number.
|
13
|
+
#
|
14
|
+
# @raise [ArgumentError] if the string does not conform to the expected format
|
15
|
+
#
|
5
16
|
def initialize(n)
|
6
17
|
case n
|
7
18
|
when String
|
@@ -11,6 +22,8 @@ module Cql
|
|
11
22
|
end
|
12
23
|
end
|
13
24
|
|
25
|
+
# Returns a string representation of this UUID in the standard 8-4-4-4-12 form.
|
26
|
+
#
|
14
27
|
def to_s
|
15
28
|
@s ||= begin
|
16
29
|
parts = []
|
@@ -23,10 +36,15 @@ module Cql
|
|
23
36
|
end
|
24
37
|
end
|
25
38
|
|
39
|
+
# Returns the numerical representation of this UUID
|
40
|
+
#
|
41
|
+
# @return [Bignum] the 128 bit numerical representation
|
42
|
+
#
|
26
43
|
def value
|
27
44
|
@n
|
28
45
|
end
|
29
46
|
|
47
|
+
# @private
|
30
48
|
def eql?(other)
|
31
49
|
self.value == other.value
|
32
50
|
end
|
@@ -34,8 +52,12 @@ module Cql
|
|
34
52
|
|
35
53
|
private
|
36
54
|
|
55
|
+
HEX_STRING_PATTERN = /^[0-9a-fA-F]+$/
|
56
|
+
|
37
57
|
def from_s(str)
|
38
58
|
str = str.gsub('-', '')
|
59
|
+
raise ArgumentError, "Expected 32 chars but got #{str.length}" unless str.length == 32
|
60
|
+
raise ArgumentError, "Expected only characters 0-9, a-f, A-F" unless str =~ HEX_STRING_PATTERN
|
39
61
|
n = 0
|
40
62
|
(str.length/2).times do |i|
|
41
63
|
n = (n << 8) | str[i * 2, 2].to_i(16)
|
data/lib/cql/version.rb
CHANGED
data/spec/cql/client_spec.rb
CHANGED
@@ -95,9 +95,25 @@ module Cql
|
|
95
95
|
|
96
96
|
it 'validates the keyspace name before sending the USE command' do
|
97
97
|
c = described_class.new(connection_options.merge(:keyspace => 'system; DROP KEYSPACE system'))
|
98
|
-
expect { c.start! }.to raise_error(InvalidKeyspaceNameError)
|
98
|
+
expect { c.start! }.to raise_error(Client::InvalidKeyspaceNameError)
|
99
99
|
requests.should_not include(Protocol::QueryRequest.new('USE system; DROP KEYSPACE system', :one))
|
100
100
|
end
|
101
|
+
|
102
|
+
it 're-raises any errors raised' do
|
103
|
+
io_reactor.stub(:add_connection).and_raise(ArgumentError)
|
104
|
+
expect { client.start! }.to raise_error(ArgumentError)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'is not connected if an error is raised' do
|
108
|
+
io_reactor.stub(:add_connection).and_raise(ArgumentError)
|
109
|
+
client.start! rescue nil
|
110
|
+
client.should_not be_connected
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'is connected after #start! returns' do
|
114
|
+
client.start!
|
115
|
+
client.should be_connected
|
116
|
+
end
|
101
117
|
end
|
102
118
|
|
103
119
|
describe '#shutdown!' do
|
@@ -153,7 +169,7 @@ module Cql
|
|
153
169
|
end
|
154
170
|
|
155
171
|
it 'raises an error if the keyspace name is not valid' do
|
156
|
-
expect { client.use('system; DROP KEYSPACE system') }.to raise_error(InvalidKeyspaceNameError)
|
172
|
+
expect { client.use('system; DROP KEYSPACE system') }.to raise_error(Client::InvalidKeyspaceNameError)
|
157
173
|
end
|
158
174
|
end
|
159
175
|
|
@@ -294,6 +310,14 @@ module Cql
|
|
294
310
|
last_request.should == Protocol::ExecuteRequest.new(id, metadata, ['foo'], :quorum)
|
295
311
|
end
|
296
312
|
|
313
|
+
it 'returns a prepared statement that knows the metadata' do
|
314
|
+
id = 'A' * 32
|
315
|
+
metadata = [['stuff', 'things', 'item', :varchar]]
|
316
|
+
io_reactor.queue_response(Protocol::PreparedResultResponse.new(id, metadata))
|
317
|
+
statement = client.prepare('SELECT * FROM stuff.things WHERE item = ?')
|
318
|
+
statement.metadata['item'].type == :varchar
|
319
|
+
end
|
320
|
+
|
297
321
|
it 'executes a prepared statement using the right connection' do
|
298
322
|
client.shutdown!
|
299
323
|
io_reactor.stop.get
|
@@ -326,34 +350,44 @@ module Cql
|
|
326
350
|
end
|
327
351
|
|
328
352
|
context 'when not connected' do
|
353
|
+
it 'is not connected before #start! has been called' do
|
354
|
+
client.should_not be_connected
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'is not connected after #shutdown! has been called' do
|
358
|
+
client.start!
|
359
|
+
client.shutdown!
|
360
|
+
client.should_not be_connected
|
361
|
+
end
|
362
|
+
|
329
363
|
it 'complains when #use is called before #start!' do
|
330
|
-
expect { client.use('system') }.to raise_error(NotConnectedError)
|
364
|
+
expect { client.use('system') }.to raise_error(Client::NotConnectedError)
|
331
365
|
end
|
332
366
|
|
333
367
|
it 'complains when #use is called after #shutdown!' do
|
334
368
|
client.start!
|
335
369
|
client.shutdown!
|
336
|
-
expect { client.use('system') }.to raise_error(NotConnectedError)
|
370
|
+
expect { client.use('system') }.to raise_error(Client::NotConnectedError)
|
337
371
|
end
|
338
372
|
|
339
373
|
it 'complains when #execute is called before #start!' do
|
340
|
-
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
374
|
+
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
341
375
|
end
|
342
376
|
|
343
377
|
it 'complains when #execute is called after #shutdown!' do
|
344
378
|
client.start!
|
345
379
|
client.shutdown!
|
346
|
-
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
380
|
+
expect { client.execute('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
347
381
|
end
|
348
382
|
|
349
383
|
it 'complains when #prepare is called before #start!' do
|
350
|
-
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
384
|
+
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
351
385
|
end
|
352
386
|
|
353
387
|
it 'complains when #prepare is called after #shutdown!' do
|
354
388
|
client.start!
|
355
389
|
client.shutdown!
|
356
|
-
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(NotConnectedError)
|
390
|
+
expect { client.prepare('DELETE FROM stuff WHERE id = 3') }.to raise_error(Client::NotConnectedError)
|
357
391
|
end
|
358
392
|
|
359
393
|
it 'complains when #execute of a prepared statement is called after #shutdown!' do
|
@@ -361,7 +395,7 @@ module Cql
|
|
361
395
|
io_reactor.queue_response(Protocol::PreparedResultResponse.new('A' * 32, []))
|
362
396
|
statement = client.prepare('DELETE FROM stuff WHERE id = 3')
|
363
397
|
client.shutdown!
|
364
|
-
expect { statement.execute }.to raise_error(NotConnectedError)
|
398
|
+
expect { statement.execute }.to raise_error(Client::NotConnectedError)
|
365
399
|
end
|
366
400
|
end
|
367
401
|
end
|
@@ -27,8 +27,13 @@ module Cql
|
|
27
27
|
end
|
28
28
|
|
29
29
|
after do
|
30
|
-
|
31
|
-
|
30
|
+
begin
|
31
|
+
if io_reactor.running?
|
32
|
+
io_reactor.stop.get
|
33
|
+
end
|
34
|
+
ensure
|
35
|
+
server.stop!
|
36
|
+
end
|
32
37
|
end
|
33
38
|
|
34
39
|
describe '#initialize' do
|
@@ -76,7 +81,9 @@ module Cql
|
|
76
81
|
end
|
77
82
|
|
78
83
|
it 'succeeds connection futures when stopping while connecting' do
|
79
|
-
|
84
|
+
server.stop!
|
85
|
+
server.start!(accept_delay: 2)
|
86
|
+
f = io_reactor.add_connection(host, port)
|
80
87
|
io_reactor.start
|
81
88
|
io_reactor.stop
|
82
89
|
f.get
|
@@ -135,7 +142,7 @@ module Cql
|
|
135
142
|
io_reactor.start
|
136
143
|
io_reactor.add_connection(host, port).get
|
137
144
|
io_reactor.queue_request(Cql::Protocol::StartupRequest.new)
|
138
|
-
|
145
|
+
await { server.received_bytes.size > 0 }
|
139
146
|
server.received_bytes[3, 1].should == "\x01"
|
140
147
|
end
|
141
148
|
|
@@ -143,7 +150,7 @@ module Cql
|
|
143
150
|
io_reactor.queue_request(Cql::Protocol::StartupRequest.new)
|
144
151
|
io_reactor.start
|
145
152
|
io_reactor.add_connection(host, port).get
|
146
|
-
|
153
|
+
await { server.received_bytes.size > 0 }
|
147
154
|
server.received_bytes[3, 1].should == "\x01"
|
148
155
|
end
|
149
156
|
|
@@ -226,6 +233,13 @@ module Cql
|
|
226
233
|
pending 'as it is the reactor doesn\'t try to deliver requests when all connections are busy'
|
227
234
|
end
|
228
235
|
|
236
|
+
it 'fails if there is an error when encoding the request' do
|
237
|
+
io_reactor.start
|
238
|
+
io_reactor.add_connection(host, port).get
|
239
|
+
f = io_reactor.queue_request(Cql::Protocol::QueryRequest.new('USE test', :foobar))
|
240
|
+
expect { f.get }.to raise_error(Cql::ProtocolError)
|
241
|
+
end
|
242
|
+
|
229
243
|
it 'yields the response when completed' do
|
230
244
|
response = nil
|
231
245
|
io_reactor.start
|
@@ -234,8 +248,9 @@ module Cql
|
|
234
248
|
f.on_complete do |r, _|
|
235
249
|
response = r
|
236
250
|
end
|
251
|
+
sleep(0.1)
|
237
252
|
server.broadcast!("\x81\x00\x00\x02\x00\x00\x00\x00")
|
238
|
-
|
253
|
+
await { response }
|
239
254
|
response.should == Cql::Protocol::ReadyResponse.new
|
240
255
|
end
|
241
256
|
|
@@ -247,8 +262,9 @@ module Cql
|
|
247
262
|
f.on_complete do |_, c|
|
248
263
|
connection = c
|
249
264
|
end
|
265
|
+
sleep(0.1)
|
250
266
|
server.broadcast!("\x81\x00\x00\x02\x00\x00\x00\x00")
|
251
|
-
|
267
|
+
await { connection }
|
252
268
|
connection.should_not be_nil
|
253
269
|
end
|
254
270
|
end
|
@@ -259,32 +275,12 @@ module Cql
|
|
259
275
|
io_reactor.start
|
260
276
|
io_reactor.add_connection(host, port).get
|
261
277
|
io_reactor.add_event_listener { |e| event = e }
|
278
|
+
sleep(0.1)
|
262
279
|
server.broadcast!("\x81\x00\xFF\f\x00\x00\x00+\x00\rSCHEMA_CHANGE\x00\aDROPPED\x00\nkeyspace01\x00\x05users")
|
263
|
-
|
280
|
+
await { event }
|
264
281
|
event.should == Cql::Protocol::SchemaChangeEventResponse.new('DROPPED', 'keyspace01', 'users')
|
265
282
|
end
|
266
283
|
end
|
267
|
-
|
268
|
-
context 'when errors occur' do
|
269
|
-
context 'in the IO loop' do
|
270
|
-
before do
|
271
|
-
bad_request = stub(:request)
|
272
|
-
bad_request.stub(:opcode).and_raise(StandardError.new('Blurgh'))
|
273
|
-
io_reactor.start
|
274
|
-
io_reactor.add_connection(host, port).get
|
275
|
-
io_reactor.queue_request(bad_request)
|
276
|
-
end
|
277
|
-
|
278
|
-
it 'stops' do
|
279
|
-
sleep(0.1)
|
280
|
-
io_reactor.should_not be_running
|
281
|
-
end
|
282
|
-
|
283
|
-
it 'fails the future returned from #stop' do
|
284
|
-
expect { io_reactor.stop.get }.to raise_error('Blurgh')
|
285
|
-
end
|
286
|
-
end
|
287
|
-
end
|
288
284
|
end
|
289
285
|
end
|
290
286
|
end
|
data/spec/cql/uuid_spec.rb
CHANGED
@@ -10,6 +10,22 @@ module Cql
|
|
10
10
|
Uuid.new('a4a70900-24e1-11df-8924-001ff3591711').to_s.should == 'a4a70900-24e1-11df-8924-001ff3591711'
|
11
11
|
end
|
12
12
|
|
13
|
+
it 'can be created from a string without hyphens' do
|
14
|
+
Uuid.new('a4a7090024e111df8924001ff3591711').to_s.should == 'a4a70900-24e1-11df-8924-001ff3591711'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'raises an error if the string is shorter than 32 chars' do
|
18
|
+
expect { Uuid.new('a4a7090024e111df8924001ff359171') }.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raises an error if the string is longer than 32 chars' do
|
22
|
+
expect { Uuid.new('a4a7090024e111df8924001ff35917111') }.to raise_error(ArgumentError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'raises an error if the string is not a hexadecimal number' do
|
26
|
+
expect { Uuid.new('a4a7090024e111df8924001ff359171x') }.to raise_error(ArgumentError)
|
27
|
+
end
|
28
|
+
|
13
29
|
it 'can be created from a number' do
|
14
30
|
Uuid.new(276263553384940695775376958868900023510).to_s.should == 'cfd66ccc-d857-4e90-b1e5-df98a3d40cd6'.force_encoding(::Encoding::ASCII)
|
15
31
|
end
|
@@ -98,4 +98,14 @@ describe 'A CQL client' do
|
|
98
98
|
end
|
99
99
|
end
|
100
100
|
end
|
101
|
+
|
102
|
+
context 'with error conditions' do
|
103
|
+
it 'raises an error for CQL syntax errors' do
|
104
|
+
expect { client.execute('BAD cql') }.to raise_error(Cql::CqlError)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'raises an error for bad consistency levels' do
|
108
|
+
expect { client.execute('SELECT * FROM system.peers', :helloworld) }.to raise_error(Cql::CqlError)
|
109
|
+
end
|
110
|
+
end
|
101
111
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module AwaitHelper
|
4
|
+
def await(timeout=5, &test)
|
5
|
+
started_at = Time.now
|
6
|
+
until test.call
|
7
|
+
yield
|
8
|
+
time_taken = Time.now - started_at
|
9
|
+
if time_taken > timeout
|
10
|
+
fail('Test took more than %.1fs' % [time_taken.to_f])
|
11
|
+
else
|
12
|
+
sleep(0.01)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec.configure do |c|
|
19
|
+
c.include(AwaitHelper)
|
20
|
+
end
|
data/spec/support/fake_server.rb
CHANGED
@@ -13,7 +13,7 @@ class FakeServer
|
|
13
13
|
@received_bytes = ''
|
14
14
|
end
|
15
15
|
|
16
|
-
def start!
|
16
|
+
def start!(options={})
|
17
17
|
@lock.synchronize do
|
18
18
|
return if @running
|
19
19
|
@running = true
|
@@ -22,6 +22,7 @@ class FakeServer
|
|
22
22
|
@started = Cql::Future.new
|
23
23
|
@thread = Thread.start do
|
24
24
|
Thread.current.abort_on_exception = true
|
25
|
+
sleep(options[:accept_delay] || 0)
|
25
26
|
@started.complete!
|
26
27
|
io_loop
|
27
28
|
end
|
@@ -34,8 +35,10 @@ class FakeServer
|
|
34
35
|
return unless @running
|
35
36
|
@running = false
|
36
37
|
end
|
37
|
-
@
|
38
|
-
|
38
|
+
if defined? @started
|
39
|
+
@thread.join
|
40
|
+
@sockets.each(&:close)
|
41
|
+
end
|
39
42
|
end
|
40
43
|
|
41
44
|
def broadcast!(bytes)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cql-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.pre1
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-02-
|
12
|
+
date: 2013-02-24 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: A pure Ruby CQL3 driver for Cassandra
|
15
15
|
email:
|
@@ -43,6 +43,7 @@ files:
|
|
43
43
|
- spec/integration/client_spec.rb
|
44
44
|
- spec/integration/protocol_spec.rb
|
45
45
|
- spec/spec_helper.rb
|
46
|
+
- spec/support/await_helper.rb
|
46
47
|
- spec/support/fake_io_reactor.rb
|
47
48
|
- spec/support/fake_server.rb
|
48
49
|
homepage: http://github.com/iconara/cql-rb
|
@@ -82,6 +83,7 @@ test_files:
|
|
82
83
|
- spec/integration/client_spec.rb
|
83
84
|
- spec/integration/protocol_spec.rb
|
84
85
|
- spec/spec_helper.rb
|
86
|
+
- spec/support/await_helper.rb
|
85
87
|
- spec/support/fake_io_reactor.rb
|
86
88
|
- spec/support/fake_server.rb
|
87
89
|
has_rdoc:
|