cql-rb 1.0.6 → 1.1.0.pre0

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.
Files changed (50) hide show
  1. data/README.md +4 -9
  2. data/lib/cql.rb +1 -0
  3. data/lib/cql/byte_buffer.rb +23 -7
  4. data/lib/cql/client.rb +11 -6
  5. data/lib/cql/client/asynchronous_client.rb +37 -83
  6. data/lib/cql/client/asynchronous_prepared_statement.rb +10 -4
  7. data/lib/cql/client/column_metadata.rb +16 -0
  8. data/lib/cql/client/request_runner.rb +46 -0
  9. data/lib/cql/future.rb +4 -5
  10. data/lib/cql/io.rb +2 -5
  11. data/lib/cql/io/connection.rb +220 -0
  12. data/lib/cql/io/io_reactor.rb +213 -185
  13. data/lib/cql/protocol.rb +1 -0
  14. data/lib/cql/protocol/cql_protocol_handler.rb +201 -0
  15. data/lib/cql/protocol/decoding.rb +6 -31
  16. data/lib/cql/protocol/encoding.rb +1 -5
  17. data/lib/cql/protocol/request.rb +4 -0
  18. data/lib/cql/protocol/responses/schema_change_result_response.rb +15 -0
  19. data/lib/cql/protocol/type_converter.rb +56 -76
  20. data/lib/cql/time_uuid.rb +104 -0
  21. data/lib/cql/uuid.rb +4 -2
  22. data/lib/cql/version.rb +1 -1
  23. data/spec/cql/client/asynchronous_client_spec.rb +47 -71
  24. data/spec/cql/client/asynchronous_prepared_statement_spec.rb +68 -0
  25. data/spec/cql/client/client_shared.rb +3 -3
  26. data/spec/cql/client/column_metadata_spec.rb +80 -0
  27. data/spec/cql/client/request_runner_spec.rb +120 -0
  28. data/spec/cql/future_spec.rb +26 -11
  29. data/spec/cql/io/connection_spec.rb +460 -0
  30. data/spec/cql/io/io_reactor_spec.rb +212 -265
  31. data/spec/cql/protocol/cql_protocol_handler_spec.rb +216 -0
  32. data/spec/cql/protocol/decoding_spec.rb +9 -28
  33. data/spec/cql/protocol/encoding_spec.rb +0 -5
  34. data/spec/cql/protocol/request_spec.rb +16 -0
  35. data/spec/cql/protocol/response_frame_spec.rb +2 -2
  36. data/spec/cql/protocol/responses/schema_change_result_response_spec.rb +70 -0
  37. data/spec/cql/time_uuid_spec.rb +136 -0
  38. data/spec/cql/uuid_spec.rb +1 -5
  39. data/spec/integration/client_spec.rb +34 -38
  40. data/spec/integration/io_spec.rb +283 -0
  41. data/spec/integration/protocol_spec.rb +53 -113
  42. data/spec/integration/regression_spec.rb +124 -0
  43. data/spec/integration/uuid_spec.rb +76 -0
  44. data/spec/spec_helper.rb +12 -9
  45. data/spec/support/fake_io_reactor.rb +52 -21
  46. data/spec/support/fake_server.rb +2 -2
  47. metadata +33 -10
  48. checksums.yaml +0 -15
  49. data/lib/cql/io/node_connection.rb +0 -209
  50. data/spec/cql/protocol/type_converter_spec.rb +0 -52
data/lib/cql/protocol.rb CHANGED
@@ -66,3 +66,4 @@ require 'cql/protocol/requests/query_request'
66
66
  require 'cql/protocol/requests/prepare_request'
67
67
  require 'cql/protocol/requests/execute_request'
68
68
  require 'cql/protocol/response_frame'
69
+ require 'cql/protocol/cql_protocol_handler'
@@ -0,0 +1,201 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Protocol
5
+ # This class wraps a single connection and translates between request/
6
+ # response frames and raw bytes.
7
+ #
8
+ # You send requests with #send_request, and receive responses through the
9
+ # returned future.
10
+ #
11
+ # Instances of this class are thread safe.
12
+ #
13
+ # @examle Sending an OPTIONS request
14
+ # future = protocol_handler.send_request(Cql::Protocol::OptionsRequest.new)
15
+ # response = future.get
16
+ # puts "These options are supported: #{response.options}"
17
+ #
18
+ class CqlProtocolHandler
19
+ # @return [String] the current keyspace for the underlying connection
20
+ attr_reader :keyspace
21
+
22
+ def initialize(connection)
23
+ @connection = connection
24
+ @connection.on_data(&method(:receive_data))
25
+ @connection.on_closed(&method(:socket_closed))
26
+ @responses = Array.new(128) { nil }
27
+ @read_buffer = ByteBuffer.new
28
+ @current_frame = Protocol::ResponseFrame.new(@read_buffer)
29
+ @request_queue_in = []
30
+ @request_queue_out = []
31
+ @event_listeners = []
32
+ @lock = Mutex.new
33
+ @closed_future = Future.new
34
+ @keyspace = nil
35
+ end
36
+
37
+ # @return [true, false] true if the underlying connection is connected
38
+ def connected?
39
+ @connection.connected?
40
+ end
41
+
42
+ # @return [true, false] true if the underlying connection is closed
43
+ def closed?
44
+ @connection.closed?
45
+ end
46
+
47
+ # Register to receive notification when the underlying connection has
48
+ # closed. If the connection closed abruptly the error will be passed
49
+ # to the listener, otherwise it will not receive any parameters.
50
+ #
51
+ # @yieldparam error [nil, Error] the error that caused the connection to
52
+ # close, if any
53
+ def on_closed(&listener)
54
+ @closed_future.on_complete(&listener)
55
+ @closed_future.on_failure(&listener)
56
+ end
57
+
58
+ # Register to receive server sent events, like schema changes, nodes going
59
+ # up or down, etc. To actually receive events you also need to send a
60
+ # REGISTER request for the events you wish to receive.
61
+ #
62
+ # @yieldparam event [Cql::Protocol::EventResponse] an event sent by the server
63
+ def on_event(&listener)
64
+ @lock.synchronize do
65
+ @event_listeners << listener
66
+ end
67
+ end
68
+
69
+ # Serializes and send a request over the underlying connection.
70
+ #
71
+ # Returns a future that will resolve to the response. When the connection
72
+ # closes the futures of all active requests will be failed with the error
73
+ # that caused the connection to close, or nil
74
+ #
75
+ # @return [Cql::Future<Cql::Protocol::Response>] a future that resolves to
76
+ # the response
77
+ def send_request(request)
78
+ return Future.failed(NotConnectedError.new) if closed?
79
+ future = Future.new
80
+ id = nil
81
+ @lock.synchronize do
82
+ if (id = next_stream_id)
83
+ @responses[id] = future
84
+ end
85
+ end
86
+ if id
87
+ @connection.write do |buffer|
88
+ request.encode_frame(id, buffer)
89
+ end
90
+ else
91
+ @lock.synchronize do
92
+ @request_queue_in << [request.encode_frame(0), future]
93
+ end
94
+ end
95
+ future
96
+ end
97
+
98
+ # Closes the underlying connection.
99
+ #
100
+ # @return [Cql::Future] a future that completes when the connection has closed
101
+ def close
102
+ @connection.close
103
+ @closed_future
104
+ end
105
+
106
+ private
107
+
108
+ def receive_data(data)
109
+ @current_frame << data
110
+ while @current_frame.complete?
111
+ id = @current_frame.stream_id
112
+ if id == -1
113
+ notify_event_listeners(@current_frame.body)
114
+ else
115
+ complete_request(id, @current_frame.body)
116
+ end
117
+ @current_frame = Protocol::ResponseFrame.new(@read_buffer)
118
+ flush_request_queue
119
+ end
120
+ end
121
+
122
+ def notify_event_listeners(event_response)
123
+ return if @event_listeners.empty?
124
+ @lock.synchronize do
125
+ @event_listeners.each do |listener|
126
+ listener.call(@current_frame.body) rescue nil
127
+ end
128
+ end
129
+ end
130
+
131
+ def complete_request(id, response)
132
+ future = @lock.synchronize do
133
+ future = @responses[id]
134
+ @responses[id] = nil
135
+ future
136
+ end
137
+ if response.is_a?(Protocol::SetKeyspaceResultResponse)
138
+ @keyspace = response.keyspace
139
+ end
140
+ future.complete!(response)
141
+ end
142
+
143
+ def flush_request_queue
144
+ @lock.synchronize do
145
+ if @request_queue_out.empty? && !@request_queue_in.empty?
146
+ @request_queue_out = @request_queue_in
147
+ @request_queue_in = []
148
+ end
149
+ end
150
+ while true
151
+ id = nil
152
+ request_buffer = nil
153
+ @lock.synchronize do
154
+ if @request_queue_out.any? && (id = next_stream_id)
155
+ request_buffer, future = @request_queue_out.shift
156
+ @responses[id] = future
157
+ end
158
+ end
159
+ if id
160
+ Protocol::Request.change_stream_id(id, request_buffer)
161
+ @connection.write(request_buffer)
162
+ else
163
+ break
164
+ end
165
+ end
166
+ end
167
+
168
+ def socket_closed(cause)
169
+ request_failure_cause = cause || Io::ConnectionClosedError.new
170
+ @lock.synchronize do
171
+ @responses.each_with_index do |future, i|
172
+ if future
173
+ @responses[i].fail!(request_failure_cause)
174
+ @responses[i] = nil
175
+ end
176
+ end
177
+ @request_queue_in.each do |_, future|
178
+ future.fail!(request_failure_cause)
179
+ end
180
+ @request_queue_in.clear
181
+ @request_queue_out.each do |_, future|
182
+ future.fail!(request_failure_cause)
183
+ end
184
+ @request_queue_out.clear
185
+ if cause
186
+ @closed_future.fail!(cause)
187
+ else
188
+ @closed_future.complete!
189
+ end
190
+ end
191
+ end
192
+
193
+ def next_stream_id
194
+ @responses.each_with_index do |task, index|
195
+ return index if task.nil?
196
+ end
197
+ nil
198
+ end
199
+ end
200
+ end
201
+ end
@@ -31,34 +31,15 @@ module Cql
31
31
  def read_decimal!(buffer, length=buffer.length)
32
32
  size = read_int!(buffer)
33
33
  number_string = read_varint!(buffer, length - 4).to_s
34
- if number_string.length < size
35
- if number_string.start_with?(MINUS)
36
- number_string = number_string[1, number_string.length - 1]
37
- fraction_string = MINUS + ZERO << DECIMAL_POINT
38
- else
39
- fraction_string = ZERO + DECIMAL_POINT
40
- end
41
- (size - number_string.length).times { fraction_string << ZERO }
42
- fraction_string << number_string
43
- else
44
- fraction_string = number_string[0, number_string.length - size]
45
- fraction_string << DECIMAL_POINT
46
- fraction_string << number_string[number_string.length - size, number_string.length]
47
- end
34
+ fraction_string = number_string[0, number_string.length - size] << DECIMAL_POINT << number_string[number_string.length - size, number_string.length]
48
35
  BigDecimal.new(fraction_string)
49
36
  rescue RangeError => e
50
37
  raise DecodingError, e.message, e.backtrace
51
38
  end
52
39
 
53
40
  def read_long!(buffer)
54
- hi, lo = buffer.read(8).unpack(Formats::TWO_INTS_FORMAT)
55
- if (hi > 0x7fffffff)
56
- hi ^= 0xffffffff
57
- lo ^= 0xffffffff
58
- 0 - (hi << 32) - lo - 1
59
- else
60
- (hi << 32) + lo
61
- end
41
+ top, bottom = buffer.read(8).unpack(Formats::TWO_INTS_FORMAT)
42
+ (top << 32) | bottom
62
43
  rescue RangeError => e
63
44
  raise DecodingError, e.message, e.backtrace
64
45
  end
@@ -76,11 +57,7 @@ module Cql
76
57
  end
77
58
 
78
59
  def read_int!(buffer)
79
- val = buffer.read_int
80
- if (val > 0x7fffffff)
81
- val = 0 - ((val - 1) ^ 0xffffffff)
82
- end
83
- val
60
+ buffer.read_int
84
61
  rescue RangeError => e
85
62
  raise DecodingError, "Not enough bytes available to decode an int: #{e.message}", e.backtrace
86
63
  end
@@ -109,8 +86,8 @@ module Cql
109
86
  raise DecodingError, "Not enough bytes available to decode a long string: #{e.message}", e.backtrace
110
87
  end
111
88
 
112
- def read_uuid!(buffer)
113
- Uuid.new(read_varint!(buffer, 16, false))
89
+ def read_uuid!(buffer, impl=Uuid)
90
+ impl.new(read_varint!(buffer, 16, false))
114
91
  rescue RangeError => e
115
92
  raise DecodingError, "Not enough bytes available to decode a UUID: #{e.message}", e.backtrace
116
93
  end
@@ -184,8 +161,6 @@ module Cql
184
161
 
185
162
  private
186
163
 
187
- MINUS = '-'.freeze
188
- ZERO = '0'.freeze
189
164
  DECIMAL_POINT = '.'.freeze
190
165
  end
191
166
  end
@@ -27,11 +27,7 @@ module Cql
27
27
  end
28
28
 
29
29
  def write_uuid(buffer, uuid)
30
- v = uuid.value
31
- write_int(buffer, (v >> 96) & 0xffffffff)
32
- write_int(buffer, (v >> 64) & 0xffffffff)
33
- write_int(buffer, (v >> 32) & 0xffffffff)
34
- write_int(buffer, v & 0xffffffff)
30
+ write_varint(buffer, uuid.value)
35
31
  end
36
32
 
37
33
  def write_string_list(buffer, strs)
@@ -19,6 +19,10 @@ module Cql
19
19
  buffer.update(offset + 4, [(buffer.bytesize - offset - 8)].pack(Formats::INT_FORMAT))
20
20
  buffer
21
21
  end
22
+
23
+ def self.change_stream_id(new_stream_id, buffer, offset=0)
24
+ buffer.update(offset + 2, new_stream_id.chr)
25
+ end
22
26
  end
23
27
  end
24
28
  end
@@ -13,6 +13,21 @@ module Cql
13
13
  new(read_string!(buffer), read_string!(buffer), read_string!(buffer))
14
14
  end
15
15
 
16
+ def eql?(other)
17
+ self.change == other.change && self.keyspace == other.keyspace && self.table == other.table
18
+ end
19
+ alias_method :==, :eql?
20
+
21
+ def hash
22
+ @h ||= begin
23
+ h = 0
24
+ h = ((h & 0xffffffff) * 31) ^ @change.hash
25
+ h = ((h & 0xffffffff) * 31) ^ @keyspace.hash
26
+ h = ((h & 0xffffffff) * 31) ^ @table.hash
27
+ h
28
+ end
29
+ end
30
+
16
31
  def to_s
17
32
  %(RESULT SCHEMA_CHANGE #@change "#@keyspace" "#@table")
18
33
  end
@@ -36,35 +36,27 @@ module Cql
36
36
  def to_bytes(io, type, value, size_bytes=4)
37
37
  case type
38
38
  when Array
39
- unless value.nil? || value.is_a?(Enumerable)
39
+ unless value.is_a?(Enumerable)
40
40
  raise InvalidValueError, 'Value for collection must be enumerable'
41
41
  end
42
42
  case type.first
43
43
  when :list, :set
44
44
  _, sub_type = type
45
- if value
46
- raw = ''
47
- write_short(raw, value.size)
48
- value.each do |element|
49
- to_bytes(raw, sub_type, element, 2)
50
- end
51
- write_bytes(io, raw)
52
- else
53
- nil_to_bytes(io, size_bytes)
45
+ raw = ''
46
+ write_short(raw, value.size)
47
+ value.each do |element|
48
+ to_bytes(raw, sub_type, element, 2)
54
49
  end
50
+ write_bytes(io, raw)
55
51
  when :map
56
52
  _, key_type, value_type = type
57
- if value
58
- raw = ''
59
- write_short(raw, value.size)
60
- value.each do |key, value|
61
- to_bytes(raw, key_type, key, 2)
62
- to_bytes(raw, value_type, value, 2)
63
- end
64
- write_bytes(io, raw)
65
- else
66
- nil_to_bytes(io, size_bytes)
53
+ raw = ''
54
+ write_short(raw, value.size)
55
+ value.each do |key, value|
56
+ to_bytes(raw, key_type, key, 2)
57
+ to_bytes(raw, value_type, value, 2)
67
58
  end
59
+ write_bytes(io, raw)
68
60
  else
69
61
  raise UnsupportedColumnTypeError, %(Unsupported column collection type: #{type.first})
70
62
  end
@@ -96,7 +88,7 @@ module Cql
96
88
  :varchar => method(:bytes_to_varchar),
97
89
  :text => method(:bytes_to_varchar),
98
90
  :varint => method(:bytes_to_varint),
99
- :timeuuid => method(:bytes_to_uuid),
91
+ :timeuuid => method(:bytes_to_timeuuid),
100
92
  :uuid => method(:bytes_to_uuid),
101
93
  :inet => method(:bytes_to_inet),
102
94
  }
@@ -108,7 +100,6 @@ module Cql
108
100
  :bigint => method(:bigint_to_bytes),
109
101
  :blob => method(:blob_to_bytes),
110
102
  :boolean => method(:boolean_to_bytes),
111
- :counter => method(:bigint_to_bytes),
112
103
  :decimal => method(:decimal_to_bytes),
113
104
  :double => method(:double_to_bytes),
114
105
  :float => method(:float_to_bytes),
@@ -197,6 +188,11 @@ module Cql
197
188
  read_uuid!(buffer)
198
189
  end
199
190
 
191
+ def bytes_to_timeuuid(buffer, size_bytes)
192
+ return nil unless read_size(buffer, size_bytes)
193
+ read_uuid!(buffer, TimeUuid)
194
+ end
195
+
200
196
  def bytes_to_inet(buffer, size_bytes)
201
197
  size = read_size(buffer, size_bytes)
202
198
  return nil unless size
@@ -233,7 +229,7 @@ module Cql
233
229
  end
234
230
 
235
231
  def ascii_to_bytes(io, value, size_bytes)
236
- v = value && value.encode(::Encoding::ASCII)
232
+ v = value.encode(::Encoding::ASCII)
237
233
  if size_bytes == 4
238
234
  write_bytes(io, v)
239
235
  else
@@ -242,16 +238,16 @@ module Cql
242
238
  end
243
239
 
244
240
  def bigint_to_bytes(io, value, size_bytes)
245
- if value
246
- size_to_bytes(io, 8, size_bytes)
247
- write_long(io, value)
241
+ if size_bytes == 4
242
+ write_int(io, 8)
248
243
  else
249
- nil_to_bytes(io, size_bytes)
244
+ write_short(io, 8)
250
245
  end
246
+ write_long(io, value)
251
247
  end
252
248
 
253
249
  def blob_to_bytes(io, value, size_bytes)
254
- v = value && value.encode(::Encoding::BINARY)
250
+ v = value.encode(::Encoding::BINARY)
255
251
  if size_bytes == 4
256
252
  write_bytes(io, v)
257
253
  else
@@ -260,16 +256,16 @@ module Cql
260
256
  end
261
257
 
262
258
  def boolean_to_bytes(io, value, size_bytes)
263
- if !value.nil?
264
- size_to_bytes(io, 1, size_bytes)
265
- io << (value ? Constants::TRUE_BYTE : Constants::FALSE_BYTE)
259
+ if size_bytes == 4
260
+ write_int(io, 1)
266
261
  else
267
- nil_to_bytes(io, size_bytes)
262
+ write_short(io, 1)
268
263
  end
264
+ io << (value ? Constants::TRUE_BYTE : Constants::FALSE_BYTE)
269
265
  end
270
266
 
271
267
  def decimal_to_bytes(io, value, size_bytes)
272
- raw = value && write_decimal('', value)
268
+ raw = write_decimal('', value)
273
269
  if size_bytes == 4
274
270
  write_bytes(io, raw)
275
271
  else
@@ -278,43 +274,43 @@ module Cql
278
274
  end
279
275
 
280
276
  def double_to_bytes(io, value, size_bytes)
281
- if value
282
- size_to_bytes(io, 8, size_bytes)
283
- write_double(io, value)
277
+ if size_bytes == 4
278
+ write_int(io, 8)
284
279
  else
285
- nil_to_bytes(io, size_bytes)
280
+ write_short(io, 8)
286
281
  end
282
+ write_double(io, value)
287
283
  end
288
284
 
289
285
  def float_to_bytes(io, value, size_bytes)
290
- if value
291
- size_to_bytes(io, 4, size_bytes)
292
- write_float(io, value)
286
+ if size_bytes == 4
287
+ write_int(io, 4)
293
288
  else
294
- nil_to_bytes(io, size_bytes)
289
+ write_short(io, 4)
295
290
  end
291
+ write_float(io, value)
296
292
  end
297
293
 
298
294
  def inet_to_bytes(io, value, size_bytes)
299
- if value
300
- size_to_bytes(io, value.ipv6? ? 16 : 4, size_bytes)
301
- io << value.hton
295
+ if size_bytes == 4
296
+ write_int(io, value.ipv6? ? 16 : 4)
302
297
  else
303
- nil_to_bytes(io, size_bytes)
298
+ write_short(io, value.ipv6? ? 16 : 4)
304
299
  end
300
+ io << value.hton
305
301
  end
306
302
 
307
303
  def int_to_bytes(io, value, size_bytes)
308
- if value
309
- size_to_bytes(io, 4, size_bytes)
310
- write_int(io, value)
304
+ if size_bytes == 4
305
+ write_int(io, 4)
311
306
  else
312
- nil_to_bytes(io, size_bytes)
307
+ write_short(io, 4)
313
308
  end
309
+ write_int(io, value)
314
310
  end
315
311
 
316
312
  def varchar_to_bytes(io, value, size_bytes)
317
- v = value && value.encode(::Encoding::UTF_8)
313
+ v = value.encode(::Encoding::UTF_8)
318
314
  if size_bytes == 4
319
315
  write_bytes(io, v)
320
316
  else
@@ -323,48 +319,32 @@ module Cql
323
319
  end
324
320
 
325
321
  def timestamp_to_bytes(io, value, size_bytes)
326
- if value
327
- ms = (value.to_f * 1000).to_i
328
- size_to_bytes(io, 8, size_bytes)
329
- write_long(io, ms)
322
+ ms = (value.to_f * 1000).to_i
323
+ if size_bytes == 4
324
+ write_int(io, 8)
330
325
  else
331
- nil_to_bytes(io, size_bytes)
326
+ write_short(io, 8)
332
327
  end
328
+ write_long(io, ms)
333
329
  end
334
330
 
335
331
  def uuid_to_bytes(io, value, size_bytes)
336
- if value
337
- size_to_bytes(io, 16, size_bytes)
338
- write_uuid(io, value)
332
+ if size_bytes == 4
333
+ write_int(io, 16)
339
334
  else
340
- nil_to_bytes(io, size_bytes)
335
+ write_short(io, 16)
341
336
  end
337
+ write_uuid(io, value)
342
338
  end
343
339
 
344
340
  def varint_to_bytes(io, value, size_bytes)
345
- raw = value && write_varint('', value)
341
+ raw = write_varint('', value)
346
342
  if size_bytes == 4
347
343
  write_bytes(io, raw)
348
344
  else
349
345
  write_short_bytes(io, raw)
350
346
  end
351
347
  end
352
-
353
- def size_to_bytes(io, size, size_bytes)
354
- if size_bytes == 4
355
- write_int(io, size)
356
- else
357
- write_short(io, size)
358
- end
359
- end
360
-
361
- def nil_to_bytes(io, size_bytes)
362
- if size_bytes == 4
363
- write_int(io, -1)
364
- else
365
- write_short(io, -1)
366
- end
367
- end
368
348
  end
369
349
  end
370
350
  end