cql-rb 1.0.0.pre4 → 1.0.0.pre5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md 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