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 CHANGED
@@ -20,15 +20,14 @@ The native transport protocol (sometimes called binary protocol, or CQL protocol
20
20
 
21
21
  require 'cql'
22
22
 
23
- client = Cql::Client.new(host: 'cassandra.example.com')
24
- client.start!
23
+ client = Cql::Client.connect(host: 'cassandra.example.com')
25
24
  client.use('system')
26
25
  rows = client.execute('SELECT keyspace_name, columnfamily_name FROM schema_columnfamilies')
27
26
  rows.each do |row|
28
27
  puts "The keyspace #{row['keyspace_name']} has a table called #{row['columnfamily_name']}""
29
28
  end
30
29
 
31
- 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.
30
+ when you're done you can call `#close` to disconnect from Cassandra. You can connect to multiple Cassandra nodes by passing multiple comma separated host names to the `:host` option.
32
31
 
33
32
  ## Changing keyspaces
34
33
 
@@ -11,15 +11,14 @@ module Cql
11
11
  end
12
12
 
13
13
  ClientError = Class.new(CqlError)
14
+ AuthenticationError = Class.new(ClientError)
14
15
 
15
16
  # A CQL client manages connections to one or more Cassandra nodes and you use
16
17
  # it run queries, insert and update data.
17
18
  #
18
19
  # @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!
20
+ # # create a client and connect to two Cassandra nodes
21
+ # client = Cql::Client.connect(host: 'node01.cassandra.local,node02.cassandra.local')
23
22
  # # change to a keyspace
24
23
  # client.use('stuff')
25
24
  #
@@ -47,8 +46,8 @@ module Cql
47
46
  # Create a new client.
48
47
  #
49
48
  # 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`.
49
+ # call {#connect} to connect, or use {Client.connect}. `#connect` returns
50
+ # `self` so you can chain that call after `new`.
52
51
  #
53
52
  # @param [Hash] options
54
53
  # @option options [String] :host ('localhost') One or more (comma separated)
@@ -67,9 +66,14 @@ module Cql
67
66
  @started = false
68
67
  @shut_down = false
69
68
  @initial_keyspace = options[:keyspace]
69
+ @credentials = options[:credentials]
70
70
  @connection_keyspaces = {}
71
71
  end
72
72
 
73
+ def self.connect(options={})
74
+ new(options).connect
75
+ end
76
+
73
77
  # Connect to all nodes.
74
78
  #
75
79
  # You must call this method before you call any of the other methods of a
@@ -81,29 +85,37 @@ module Cql
81
85
  #
82
86
  # @return self
83
87
  #
84
- def start!
88
+ def connect
85
89
  @lock.synchronize do
86
90
  return if @started
91
+ @started = true
87
92
  @io_reactor.start
88
93
  hosts = @host.split(',')
89
- start_request = Protocol::StartupRequest.new
90
- connection_futures = hosts.map do |host|
91
- @io_reactor.add_connection(host, @port).flat_map do |connection_id|
92
- execute_request(start_request, connection_id).map { connection_id }
93
- end
94
- end
94
+ connection_futures = hosts.map { |host| connect_to_host(host) }
95
95
  @connection_ids = Future.combine(*connection_futures).get
96
- @started = true
97
96
  end
98
97
  use(@initial_keyspace) if @initial_keyspace
99
98
  self
99
+ rescue => e
100
+ close
101
+ if e.is_a?(Cql::QueryError) && e.code == 0x100
102
+ raise AuthenticationError, e.message, e.backtrace
103
+ else
104
+ raise
105
+ end
106
+ end
107
+
108
+ # @deprecated Use {#connect} or {.connect}
109
+ def start!
110
+ $stderr.puts('Client#start! is deprecated, use Client#connect, or Client.connect')
111
+ connect
100
112
  end
101
113
 
102
114
  # Disconnect from all nodes.
103
115
  #
104
116
  # @return self
105
117
  #
106
- def shutdown!
118
+ def close
107
119
  @lock.synchronize do
108
120
  return if @shut_down || !@started
109
121
  @shut_down = true
@@ -113,6 +125,12 @@ module Cql
113
125
  self
114
126
  end
115
127
 
128
+ # @deprecated Use {#close}
129
+ def shutdown!
130
+ $stderr.puts('Client#shutdown! is deprecated, use Client#close')
131
+ close
132
+ end
133
+
116
134
  # Returns whether or not the client is connected.
117
135
  #
118
136
  def connected?
@@ -196,6 +214,28 @@ module Cql
196
214
  true
197
215
  end
198
216
 
217
+ def connect_to_host(host)
218
+ connected = @io_reactor.add_connection(host, @port)
219
+ connected.flat_map do |connection_id|
220
+ started = execute_request(Protocol::StartupRequest.new, connection_id)
221
+ started.flat_map { |response| maybe_authenticate(response, connection_id) }
222
+ end
223
+ end
224
+
225
+ def maybe_authenticate(response, connection_id)
226
+ case response
227
+ when AuthenticationRequired
228
+ if @credentials
229
+ credentials_request = Protocol::CredentialsRequest.new(@credentials)
230
+ execute_request(credentials_request, connection_id).map { connection_id }
231
+ else
232
+ Future.failed(AuthenticationError.new('Server requested authentication, but no credentials given'))
233
+ end
234
+ else
235
+ Future.completed(connection_id)
236
+ end
237
+ end
238
+
199
239
  def execute_request(request, connection_id=nil)
200
240
  @io_reactor.queue_request(request, connection_id).map do |response, connection_id|
201
241
  interpret_response!(response, connection_id)
@@ -215,6 +255,8 @@ module Cql
215
255
  @last_keyspace_change = @connection_keyspaces[connection_id] = response.keyspace
216
256
  end
217
257
  nil
258
+ when Protocol::AuthenticateResponse
259
+ AuthenticationRequired.new(response.authentication_class)
218
260
  else
219
261
  nil
220
262
  end
@@ -264,6 +306,14 @@ module Cql
264
306
  end
265
307
  end
266
308
 
309
+ class AuthenticationRequired
310
+ attr_reader :authentication_class
311
+
312
+ def initialize(authentication_class)
313
+ @authentication_class = authentication_class
314
+ end
315
+ end
316
+
267
317
  class QueryResult
268
318
  include Enumerable
269
319
 
@@ -5,6 +5,7 @@ module Cql
5
5
 
6
6
  module Io
7
7
  ConnectionError = Class.new(IoError)
8
+ ConnectionTimeoutError = Class.new(ConnectionError)
8
9
  NotRunningError = Class.new(CqlError)
9
10
  ConnectionNotFoundError = Class.new(CqlError)
10
11
  ConnectionBusyError = Class.new(CqlError)
@@ -12,3 +13,4 @@ module Cql
12
13
  end
13
14
 
14
15
  require 'cql/io/io_reactor'
16
+ require 'cql/io/node_connection'
@@ -1,9 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'socket'
4
- require 'resolv-replace'
5
-
6
-
7
3
  module Cql
8
4
  module Io
9
5
  # An instance of IO reactor manages the connections used by a client.
@@ -20,9 +16,9 @@ module Cql
20
16
  def initialize(options={})
21
17
  @connection_timeout = options[:connection_timeout] || 5
22
18
  @lock = Mutex.new
23
- @streams = []
24
19
  @command_queue = []
25
- @queue_signal_receiver, @queue_signal_sender = IO.pipe
20
+ @unblocker = UnblockerConnection.new(*IO.pipe)
21
+ @connections = [@unblocker]
26
22
  @started_future = Future.new
27
23
  @stopped_future = Future.new
28
24
  @running = false
@@ -45,7 +41,6 @@ module Cql
45
41
  @lock.synchronize do
46
42
  unless @running
47
43
  @running = true
48
- @streams << CommandDispatcher.new(@queue_signal_receiver, @command_queue, @lock, @streams)
49
44
  @reactor_thread = Thread.start do
50
45
  begin
51
46
  @started_future.complete!
@@ -70,6 +65,7 @@ module Cql
70
65
  #
71
66
  def stop
72
67
  @running = false
68
+ command_queue_push(nil)
73
69
  @stopped_future
74
70
  end
75
71
 
@@ -82,20 +78,17 @@ module Cql
82
78
  #
83
79
  def add_connection(host, port)
84
80
  connection = NodeConnection.new(host, port, @connection_timeout)
85
- future = connection.open
86
- future.on_failure do
81
+ connection.on_close do
87
82
  @lock.synchronize do
88
- @streams.delete(connection)
83
+ @connections.delete(connection)
89
84
  end
90
85
  end
91
- future.on_complete do
92
- command_queue_push(:connection_established, connection)
93
- end
86
+ f = connection.open
94
87
  @lock.synchronize do
95
- @streams << connection
88
+ @connections << connection
96
89
  end
97
- command_queue_push(:connection_added, connection)
98
- future
90
+ command_queue_push(nil)
91
+ f
99
92
  end
100
93
 
101
94
  # Sends a request over a random, or specific connection.
@@ -106,9 +99,9 @@ module Cql
106
99
  # @return [Future<ResultResponse>] a future representing the result of the request
107
100
  #
108
101
  def queue_request(request, connection_id=nil)
109
- future = Future.new
110
- command_queue_push(:request, request, future, connection_id)
111
- future
102
+ command = connection_id ? TargetedRequestCommand.new(request, connection_id) : RequestCommand.new(request)
103
+ command_queue_push(command)
104
+ command.future
112
105
  end
113
106
 
114
107
  # Registers a listener to receive server sent events.
@@ -116,258 +109,133 @@ module Cql
116
109
  # @yieldparam [Cql::Protocol::EventResponse] event the event sent by the server
117
110
  #
118
111
  def add_event_listener(&listener)
119
- command_queue_push(:event_listener, listener)
112
+ command_queue_push(EventListenerCommand.new(listener))
120
113
  end
121
114
 
122
115
  private
123
116
 
124
- PING_BYTE = "\0".freeze
125
-
126
117
  def io_loop
127
118
  while running?
128
- read_ready_streams = @streams.select(&:connected?)
129
- write_ready_streams = @streams.select(&:can_write?)
119
+ read_ready_streams = @connections.select(&:connected?)
120
+ write_ready_streams = @connections.select(&:can_write?)
130
121
  readables, writables, _ = IO.select(read_ready_streams, write_ready_streams, nil, 1)
131
122
  readables && readables.each(&:handle_read)
132
123
  writables && writables.each(&:handle_write)
133
- @streams.each(&:ping)
134
- @streams.reject!(&:closed?)
124
+ @connections.each do |connection|
125
+ if connection.connecting?
126
+ connection.handle_connecting
127
+ end
128
+ end
129
+ if running?
130
+ perform_queued_commands
131
+ end
135
132
  end
136
133
  ensure
137
134
  stop
138
- @streams.each do |stream|
135
+ @connections.dup.each do |connection|
139
136
  begin
140
- stream.close
141
- rescue IOError
137
+ connection.close
138
+ rescue IOError => e
142
139
  end
143
140
  end
144
141
  end
145
142
 
146
- def command_queue_push(*item)
147
- if item && item.any?
143
+ def command_queue_push(command)
144
+ if command
148
145
  @lock.synchronize do
149
- @command_queue << item
146
+ @command_queue << command
150
147
  end
151
148
  end
152
- @queue_signal_sender.write(PING_BYTE)
153
- end
154
- end
155
-
156
- # @private
157
- class NodeConnection
158
- def initialize(*args)
159
- @host, @port, @connection_timeout = args
160
- @connected_future = Future.new
161
- @io = nil
162
- @addrinfo = nil
163
- @write_buffer = ''
164
- @read_buffer = ''
165
- @current_frame = Protocol::ResponseFrame.new(@read_buffer)
166
- @response_tasks = [nil] * 128
167
- @event_listeners = Hash.new { |h, k| h[k] = [] }
149
+ @unblocker.unblock!
168
150
  end
169
151
 
170
- def open
171
- @connection_started_at = Time.now
172
- begin
173
- addrinfo = Socket.getaddrinfo(@host, @port, Socket::AF_INET, Socket::SOCK_STREAM)
174
- _, port, _, ip, address_family, socket_type = addrinfo.first
175
- @sockaddr = Socket.sockaddr_in(port, ip)
176
- @io = Socket.new(address_family, socket_type, 0)
177
- @io.connect_nonblock(@sockaddr)
178
- rescue Errno::EINPROGRESS
179
- # ok
180
- rescue SystemCallError, SocketError => e
181
- fail_connection!(e)
152
+ def perform_queued_commands
153
+ @lock.synchronize do
154
+ unexecuted_commands = []
155
+ while (command = @command_queue.shift)
156
+ case command
157
+ when EventListenerCommand
158
+ @connections.each do |connection|
159
+ connection.on_event(&command.listener)
160
+ end
161
+ when TargetedRequestCommand
162
+ connection = @connections.find { |c| c.connection_id == command.connection_id }
163
+ if connection && connection.connected? && connection.has_capacity?
164
+ connection.perform_request(command.request, command.future)
165
+ elsif connection && connection.connected?
166
+ command.future.fail!(ConnectionBusyError.new("Connection ##{command.connection_id} is busy"))
167
+ else
168
+ command.future.fail!(ConnectionNotFoundError.new("Connection ##{command.connection_id} does not exist"))
169
+ end
170
+ when RequestCommand
171
+ connection = @connections.select(&:has_capacity?).sample
172
+ if connection
173
+ connection.perform_request(command.request, command.future)
174
+ else
175
+ unexecuted_commands << command
176
+ end
177
+ end
178
+ end
179
+ @command_queue.unshift(*unexecuted_commands) if unexecuted_commands.any?
182
180
  end
183
- @connected_future
184
- end
185
-
186
- def connection_id
187
- self.object_id
188
- end
189
-
190
- def to_io
191
- @io
192
- end
193
-
194
- def on_event(&listener)
195
- @event_listeners[:event] << listener
196
- end
197
-
198
- def on_close(&listener)
199
- @event_listeners[:close] << listener
200
181
  end
182
+ end
201
183
 
202
- def ping
203
- if @io && connecting? && (Time.now - @connection_started_at > @connection_timeout)
204
- fail_connection!
205
- end
206
- end
184
+ class EventListenerCommand
185
+ attr_reader :listener
207
186
 
208
- def connected?
209
- @io && !connecting?
187
+ def initialize(listener)
188
+ @listener = listener
210
189
  end
190
+ end
211
191
 
212
- def closed?
213
- @io.nil? && !connecting?
214
- end
192
+ class RequestCommand
193
+ attr_reader :future, :request
215
194
 
216
- def has_capacity?
217
- !!next_stream_id && connected?
195
+ def initialize(request)
196
+ @request = request
197
+ @future = Future.new
218
198
  end
199
+ end
219
200
 
220
- def can_write?
221
- @io && (!@write_buffer.empty? || connecting?)
222
- end
201
+ class TargetedRequestCommand < RequestCommand
202
+ attr_reader :connection_id
223
203
 
224
- def perform_request(request, future)
225
- stream_id = next_stream_id
226
- Protocol::RequestFrame.new(request, stream_id).write(@write_buffer)
227
- @response_tasks[stream_id] = future
228
- rescue => e
229
- case e
230
- when CqlError
231
- error = e
232
- else
233
- error = IoError.new(e.message)
234
- error.set_backtrace(e.backtrace)
235
- end
236
- @response_tasks.delete(stream_id)
237
- future.fail!(error)
204
+ def initialize(request, connection_id)
205
+ super(request)
206
+ @connection_id = connection_id
238
207
  end
208
+ end
239
209
 
240
- def handle_read
241
- new_bytes = @io.read_nonblock(2**16)
242
- @current_frame << new_bytes
243
- while @current_frame.complete?
244
- stream_id = @current_frame.stream_id
245
- if stream_id == EVENT_STREAM_ID
246
- @event_listeners[:event].each { |listener| listener.call(@current_frame.body) }
247
- elsif @response_tasks[stream_id]
248
- @response_tasks[stream_id].complete!([@current_frame.body, connection_id])
249
- @response_tasks[stream_id] = nil
250
- else
251
- # TODO dropping the request on the floor here, but we didn't send it
252
- end
253
- @current_frame = Protocol::ResponseFrame.new(@read_buffer)
254
- end
255
- rescue => e
256
- force_close(e)
210
+ class UnblockerConnection
211
+ def initialize(*args)
212
+ @out, @in = args
257
213
  end
258
214
 
259
- def handle_write
260
- if connecting?
261
- handle_connected
262
- elsif connected?
263
- bytes_written = @io.write_nonblock(@write_buffer)
264
- @write_buffer.slice!(0, bytes_written)
265
- end
266
- rescue => e
267
- force_close(e)
215
+ def unblock!
216
+ @in.write(PING_BYTE)
268
217
  end
269
218
 
270
- def force_close(e)
271
- case e
272
- when CqlError
273
- error = e
274
- else
275
- error = IoError.new(e.message)
276
- error.set_backtrace(e.backtrace)
277
- end
278
- @response_tasks.each do |listener|
279
- listener.fail!(error) if listener
280
- end
281
- close
219
+ def to_io
220
+ @out
282
221
  end
283
222
 
284
223
  def close
285
- if @io
286
- begin
287
- @io.close
288
- rescue SystemCallError
289
- # nothing to do, it wasn't open
290
- end
291
- @io = nil
292
- if connecting?
293
- succeed_connection!
294
- end
295
- @event_listeners[:close].each { |listener| listener.call(self) }
296
- end
297
224
  end
298
225
 
299
- def to_s
300
- state = begin
301
- if connected? then 'connected'
302
- elsif connecting? then 'connecting'
303
- else 'not connected'
304
- end
305
- end
306
- %<NodeConnection(#{@host}:#{@port}, #{state})>
307
- end
308
-
309
- private
310
-
311
- EVENT_STREAM_ID = -1
312
-
313
- def connecting?
314
- !(@connected_future.complete? || @connected_future.failed?)
315
- end
316
-
317
- def handle_connected
318
- @io.connect_nonblock(@sockaddr)
319
- succeed_connection!
320
- rescue Errno::EISCONN
321
- # ok
322
- succeed_connection!
323
- rescue SystemCallError, SocketError => e
324
- fail_connection!(e)
325
- end
326
-
327
- def succeed_connection!
328
- @connected_future.complete!(connection_id)
329
- end
330
-
331
- def fail_connection!(e=nil)
332
- message = "Could not connect to #{@host}:#{@port}"
333
- message << ": #{e.message} (#{e.class.name})" if e
334
- error = ConnectionError.new(message)
335
- error.set_backtrace(e.backtrace) if e
336
- @connected_future.fail!(error)
337
- close
338
- end
339
-
340
- def next_stream_id
341
- @response_tasks.each_with_index do |task, index|
342
- return index if task.nil?
343
- end
344
- nil
345
- end
346
- end
226
+ def on_event; end
347
227
 
348
- # @private
349
- class CommandDispatcher
350
- def initialize(*args)
351
- @io, @command_queue, @queue_lock, @node_connections = args
352
- end
228
+ def on_close; end
353
229
 
354
230
  def connection_id
355
231
  -1
356
232
  end
357
233
 
358
- def to_io
359
- @io
360
- end
361
-
362
234
  def connected?
363
235
  true
364
236
  end
365
237
 
366
- def closed?
367
- false
368
- end
369
-
370
- def has_capacity?
238
+ def connecting?
371
239
  false
372
240
  end
373
241
 
@@ -375,99 +243,17 @@ module Cql
375
243
  false
376
244
  end
377
245
 
378
- def on_event; end
379
-
380
- def on_close; end
381
-
382
- def ping
383
- if can_deliver_command?
384
- deliver_commands
385
- else
386
- prune_directed_requests!
387
- end
246
+ def has_capacity?
247
+ false
388
248
  end
389
249
 
390
250
  def handle_read
391
- if @io.read_nonblock(1)
392
- deliver_commands
393
- end
394
- end
395
-
396
- def handle_write
397
- end
398
-
399
- def close
400
- @io.close
401
- end
402
-
403
- def to_s
404
- %(CommandDispatcher)
251
+ @out.read_nonblock(2**16)
405
252
  end
406
253
 
407
254
  private
408
255
 
409
- def can_deliver_command?
410
- @node_connections.any?(&:has_capacity?) && @command_queue.size > 0
411
- end
412
-
413
- def next_command
414
- @queue_lock.synchronize do
415
- if can_deliver_command?
416
- return @command_queue.shift
417
- end
418
- end
419
- nil
420
- end
421
-
422
- def deliver_commands
423
- while (command = next_command)
424
- case command.shift
425
- when :connection_added
426
- when :connection_established
427
- connection = command.shift
428
- connection.on_close(&method(:connection_closed))
429
- when :event_listener
430
- listener = command.shift
431
- @node_connections.each { |c| c.on_event(&listener) }
432
- when :request
433
- request, future, connection_id = command
434
- if connection_id
435
- connection = @node_connections.find { |c| c.connection_id == connection_id }
436
- if connection && connection.connected? && connection.has_capacity?
437
- connection.perform_request(request, future)
438
- elsif connection && connection.connected?
439
- future.fail!(ConnectionBusyError.new("Connection ##{connection_id} is busy"))
440
- else
441
- future.fail!(ConnectionNotFoundError.new("Connection ##{connection_id} does not exist"))
442
- end
443
- else
444
- @node_connections.select(&:has_capacity?).sample.perform_request(request, future)
445
- end
446
- end
447
- end
448
- end
449
-
450
- def prune_directed_requests!
451
- failing_commands = []
452
- @queue_lock.synchronize do
453
- @command_queue.reject! do |command|
454
- if command.first == :request && command.last && @node_connections.none? { |c| c.connection_id == command.last }
455
- failing_commands << command
456
- true
457
- else
458
- false
459
- end
460
- end
461
- end
462
- failing_commands.each do |command|
463
- _, _, future, id = command
464
- future.fail!(ConnectionNotFoundError.new("Connection ##{id} no longer exists"))
465
- end
466
- end
467
-
468
- def connection_closed(connection)
469
- prune_directed_requests!
470
- end
256
+ PING_BYTE = "\0".freeze
471
257
  end
472
258
  end
473
259
  end