cassandra-driver 1.0.0.rc.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +45 -10
  3. data/lib/cassandra.rb +82 -82
  4. data/lib/cassandra/cluster.rb +3 -0
  5. data/lib/cassandra/cluster/client.rb +17 -5
  6. data/lib/cassandra/{client/connection_manager.rb → cluster/connection_pool.rb} +3 -3
  7. data/lib/cassandra/cluster/connector.rb +101 -30
  8. data/lib/cassandra/cluster/control_connection.rb +6 -7
  9. data/lib/cassandra/{client/null_logger.rb → cluster/failed_connection.rb} +12 -14
  10. data/lib/cassandra/cluster/options.rb +8 -0
  11. data/lib/cassandra/column.rb +5 -0
  12. data/lib/cassandra/driver.rb +3 -3
  13. data/lib/cassandra/errors.rb +5 -5
  14. data/lib/cassandra/execution/options.rb +13 -6
  15. data/lib/cassandra/execution/trace.rb +4 -4
  16. data/lib/cassandra/future.rb +4 -0
  17. data/lib/cassandra/keyspace.rb +5 -0
  18. data/lib/cassandra/load_balancing/policies/dc_aware_round_robin.rb +7 -2
  19. data/lib/cassandra/load_balancing/policies/token_aware.rb +1 -3
  20. data/lib/cassandra/load_balancing/policies/white_list.rb +3 -6
  21. data/lib/cassandra/null_logger.rb +35 -0
  22. data/lib/cassandra/protocol/cql_protocol_handler.rb +4 -0
  23. data/lib/cassandra/protocol/requests/query_request.rb +1 -11
  24. data/lib/cassandra/result.rb +4 -6
  25. data/lib/cassandra/session.rb +3 -0
  26. data/lib/cassandra/statements/prepared.rb +5 -1
  27. data/lib/cassandra/table.rb +5 -0
  28. data/lib/cassandra/util.rb +130 -0
  29. data/lib/cassandra/version.rb +1 -1
  30. metadata +9 -20
  31. data/lib/cassandra/client.rb +0 -144
  32. data/lib/cassandra/client/batch.rb +0 -212
  33. data/lib/cassandra/client/client.rb +0 -591
  34. data/lib/cassandra/client/column_metadata.rb +0 -54
  35. data/lib/cassandra/client/connector.rb +0 -273
  36. data/lib/cassandra/client/execute_options_decoder.rb +0 -59
  37. data/lib/cassandra/client/peer_discovery.rb +0 -50
  38. data/lib/cassandra/client/prepared_statement.rb +0 -314
  39. data/lib/cassandra/client/query_result.rb +0 -230
  40. data/lib/cassandra/client/request_runner.rb +0 -70
  41. data/lib/cassandra/client/result_metadata.rb +0 -48
  42. data/lib/cassandra/client/void_result.rb +0 -78
@@ -1,144 +0,0 @@
1
- # encoding: utf-8
2
-
3
- #--
4
- # Copyright 2013-2014 DataStax, Inc.
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #++
18
-
19
- module Cassandra
20
- # A CQL client manages connections to one or more Cassandra nodes and you use
21
- # it run queries, insert and update data.
22
- #
23
- # Client instances are threadsafe.
24
- #
25
- # See {Cassandra::Client::Client} for the full client API, or {Cassandra::Client.connect}
26
- # for the options available when connecting.
27
- #
28
- # @example Connecting and changing to a keyspace
29
- # # create a client and connect to two Cassandra nodes
30
- # client = Cassandra::Client.connect(hosts: %w[node01.cassandra.local node02.cassandra.local])
31
- # # change to a keyspace
32
- # client.use('stuff')
33
- #
34
- # @example Query for data
35
- # rows = client.execute('SELECT * FROM things WHERE id = 2')
36
- # rows.each do |row|
37
- # p row
38
- # end
39
- #
40
- # @example Inserting and updating data
41
- # client.execute("INSERT INTO things (id, value) VALUES (4, 'foo')")
42
- # client.execute("UPDATE things SET value = 'bar' WHERE id = 5")
43
- #
44
- # @example Prepared statements
45
- # statement = client.prepare('INSERT INTO things (id, value) VALUES (?, ?)')
46
- # statement.execute(9, 'qux')
47
- # statement.execute(8, 'baz')
48
- # @private
49
- module Client
50
- InvalidKeyspaceNameError = Class.new(Errors::ClientError)
51
-
52
- # @private
53
- module SynchronousBacktrace
54
- def synchronous_backtrace
55
- yield
56
- rescue Error => e
57
- new_backtrace = caller
58
- if new_backtrace.first.include?(SYNCHRONOUS_BACKTRACE_METHOD_NAME)
59
- new_backtrace = new_backtrace.drop(1)
60
- end
61
- e.set_backtrace(new_backtrace)
62
- raise
63
- end
64
-
65
- private
66
-
67
- SYNCHRONOUS_BACKTRACE_METHOD_NAME = 'synchronous_backtrace'
68
- end
69
-
70
- # Create a new client and connect to Cassandra.
71
- #
72
- # By default the client will connect to localhost port 9042, which can be
73
- # overridden with the `:hosts` and `:port` options, respectively. Once
74
- # connected to the hosts given in `:hosts` the rest of the nodes in the
75
- # cluster will automatically be discovered and connected to.
76
- #
77
- # If you have a multi data center setup the client will connect to all nodes
78
- # in the data centers where the nodes you pass to `:hosts` are located. So
79
- # if you only want to connect to nodes in one data center, make sure that
80
- # you only specify nodes in that data center in `:hosts`.
81
- #
82
- # The connection will succeed if at least one node is up and accepts the
83
- # connection. Nodes that don't respond within the specified timeout, or
84
- # where the connection initialization fails for some reason, are ignored.
85
- #
86
- # @param [Hash] options
87
- # @option options [Array<String>] :hosts (['localhost']) One or more
88
- # hostnames used as seed nodes when connecting. Duplicates will be removed.
89
- # @option options [String] :port (9042) The port to connect to, this port
90
- # will be used for all nodes. Because the `system.peers` table does not
91
- # contain the port that the nodes are listening on, the port must be the
92
- # same for all nodes.
93
- # @option options [Integer] :connection_timeout (5) Max time to wait for a
94
- # connection, in seconds.
95
- # @option options [String] :keyspace The keyspace to change to immediately
96
- # after all connections have been established, this is optional.
97
- # @option options [Hash] :credentials When using Cassandra's built in
98
- # authentication you can provide your username and password through this
99
- # option. Example: `:credentials => {:username => 'cassandra', :password => 'cassandra'}`
100
- # @option options [Object] :auth_provider When using custom authentication
101
- # use this option to specify the auth provider that will handle the
102
- # authentication negotiation. See {Cassandra::Client::AuthProvider} for more info.
103
- # @option options [Integer] :connections_per_node (1) The number of
104
- # connections to open to each node. Each connection can have 128
105
- # concurrent requests, so unless you have a need for more than that (times
106
- # the number of nodes in your cluster), leave this option at its default.
107
- # @option options [Integer] :default_consistency (:quorum) The consistency
108
- # to use unless otherwise specified. Consistency can also be specified on
109
- # a per-request basis.
110
- # @option options [Cassandra::Compression::Compressor] :compressor An object that
111
- # can compress and decompress frames. By specifying this option frame
112
- # compression will be enabled. If the server does not support compression
113
- # or the specific compression algorithm specified by the compressor,
114
- # compression will not be enabled and a warning will be logged.
115
- # @option options [String] :cql_version Specifies which CQL version the
116
- # server should expect.
117
- # @option options [Integer] :logger If you want the client to log
118
- # significant events pass an object implementing the standard Ruby logger
119
- # interface (e.g. quacks like `Logger` from the standard library) with
120
- # this option.
121
- # @raise [Cassandra::Errors::IOError] when a connection couldn't be established
122
- # to any node
123
- # @raise [Cassandra::Errors::ExecutionError] when the specified keyspace does not exist
124
- # or when the specifed CQL version is not supported.
125
- # @return [Cassandra::Client::Client]
126
- def self.connect(options={})
127
- SynchronousClient.new(AsynchronousClient.new(options)).connect
128
- end
129
- end
130
- end
131
-
132
- require 'cassandra/client/connection_manager'
133
- require 'cassandra/client/connector'
134
- require 'cassandra/client/null_logger'
135
- require 'cassandra/client/column_metadata'
136
- require 'cassandra/client/result_metadata'
137
- require 'cassandra/client/execute_options_decoder'
138
- require 'cassandra/client/client'
139
- require 'cassandra/client/prepared_statement'
140
- require 'cassandra/client/batch'
141
- require 'cassandra/client/query_result'
142
- require 'cassandra/client/void_result'
143
- require 'cassandra/client/request_runner'
144
- require 'cassandra/client/peer_discovery'
@@ -1,212 +0,0 @@
1
- # encoding: utf-8
2
-
3
- #--
4
- # Copyright 2013-2014 DataStax, Inc.
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #++
18
-
19
- module Cassandra
20
- module Client
21
- # Batches let you send multiple queries (`INSERT`, `UPDATE` and `DELETE`) in
22
- # one go. This can lead to better performance, and depending on the options
23
- # you specify can also give you different consistency guarantees.
24
- #
25
- # Batches can contain a mix of different queries and prepared statements.
26
- #
27
- # @see Cassandra::Client::Client#batch
28
- class Batch
29
- # @!method add(cql_or_prepared_statement, *bound_values)
30
- #
31
- # Add a query or a prepared statement to the batch.
32
- #
33
- # @example Adding a mix of statements to a batch
34
- # batch.add(%(UPDATE people SET name = 'Miriam' WHERE id = 3435))
35
- # batch.add(%(UPDATE people SET name = ? WHERE id = ?), 'Miriam', 3435)
36
- # batch.add(prepared_statement, 'Miriam', 3435)
37
- #
38
- # @param [String, Cassandra::Client::PreparedStatement] cql_or_prepared_statement
39
- # a CQL string or a prepared statement object (obtained through
40
- # {Cassandra::Client::Client#prepare})
41
- # @param [Array] bound_values a list of bound values -- only applies when
42
- # adding prepared statements and when there are binding markers in the
43
- # given CQL. If the last argument is a hash and it has the key
44
- # `:type_hints` this will be passed as type hints to the request encoder
45
- # (if the last argument is any other hash it will be assumed to be a
46
- # bound value of type MAP). See {Cassandra::Client::Client#execute} for more
47
- # info on type hints.
48
- # @return [nil]
49
-
50
- # @!method execute(options={})
51
- #
52
- # Execute the batch and return the result.
53
- #
54
- # @param [Hash] options an options hash or a symbol (as a shortcut for
55
- # specifying the consistency), see {Cassandra::Client::Client#execute} for
56
- # full details about how this value is interpreted.
57
- # @raise [Cassandra::Errors::ExecutionError] raised when there is an error on the server side
58
- # @raise [Cassandra::Errors::NotPreparedError] raised in the unlikely event that a
59
- # prepared statement was not prepared on the chosen connection
60
- # @return [Cassandra::Client::VoidResult] a batch always returns a void result
61
- end
62
-
63
- # A convenient wrapper that makes it easy to build batches of multiple
64
- # executions of the same prepared statement.
65
- #
66
- # @see Cassandra::Client::PreparedStatement#batch
67
- class PreparedStatementBatch
68
- # @!method add(*bound_values)
69
- #
70
- # Add the statement to the batch with the specified bound values.
71
- #
72
- # @param [Array] bound_values the values to bind to the added statement,
73
- # see {Cassandra::Client::PreparedStatement#execute}.
74
- # @return [nil]
75
-
76
- # @!method execute(options={})
77
- #
78
- # Execute the batch and return the result.
79
- #
80
- # @raise [Cassandra::Errors::ExecutionError] raised when there is an error on the server side
81
- # @raise [Cassandra::Errors::NotPreparedError] raised in the unlikely event that a
82
- # prepared statement was not prepared on the chosen connection
83
- # @return [Cassandra::Client::VoidResult] a batch always returns a void result
84
- end
85
-
86
- # @private
87
- class AsynchronousBatch < Batch
88
- def initialize(type, execute_options_decoder, connection_manager, options=nil)
89
- raise ::ArgumentError, "Unknown batch type: #{type}" unless BATCH_TYPES.include?(type)
90
- @type = type
91
- @execute_options_decoder = execute_options_decoder
92
- @connection_manager = connection_manager
93
- @options = options
94
- @request_runner = RequestRunner.new
95
- @parts = []
96
- end
97
-
98
- def add(*args)
99
- @parts << args
100
- nil
101
- end
102
-
103
- def execute(options=nil)
104
- options = @execute_options_decoder.decode_options(@options, options)
105
- connection = @connection_manager.random_connection
106
- request = Protocol::BatchRequest.new(BATCH_TYPES[@type], options[:consistency], options[:trace])
107
- unprepared_statements = nil
108
- @parts.each do |part, *bound_args|
109
- if part.is_a?(String) || part.prepared?(connection)
110
- add_part(connection, request, part, bound_args)
111
- else
112
- unprepared_statements ||= []
113
- unprepared_statements << [part, bound_args]
114
- end
115
- end
116
- @parts = []
117
- if unprepared_statements.nil?
118
- @request_runner.execute(connection, request, options[:timeout])
119
- else
120
- fs = unprepared_statements.map do |statement, _|
121
- if statement.respond_to?(:async)
122
- statement.async.prepare(connection)
123
- else
124
- statement.prepare(connection)
125
- end
126
- end
127
- Ione::Future.all(*fs).flat_map do
128
- unprepared_statements.each do |statement, bound_args|
129
- add_part(connection, request, statement, bound_args)
130
- end
131
- @request_runner.execute(connection, request, options[:timeout])
132
- end
133
- end
134
- end
135
-
136
- private
137
-
138
- BATCH_TYPES = {
139
- :logged => Protocol::BatchRequest::LOGGED_TYPE,
140
- :unlogged => Protocol::BatchRequest::UNLOGGED_TYPE,
141
- :counter => Protocol::BatchRequest::COUNTER_TYPE,
142
- }.freeze
143
-
144
- def add_part(connection, request, part, bound_args)
145
- if part.is_a?(String)
146
- type_hints = nil
147
- if bound_args.last.is_a?(Hash) && bound_args.last.include?(:type_hints)
148
- bound_args = bound_args.dup
149
- type_hints = bound_args.pop[:type_hints]
150
- end
151
- request.add_query(part, bound_args, type_hints)
152
- else
153
- part.add_to_batch(request, connection, bound_args)
154
- end
155
- end
156
- end
157
-
158
- # @private
159
- class SynchronousBatch < Batch
160
- include SynchronousBacktrace
161
-
162
- def initialize(asynchronous_batch)
163
- @asynchronous_batch = asynchronous_batch
164
- end
165
-
166
- def async
167
- @asynchronous_batch
168
- end
169
-
170
- def add(*args)
171
- @asynchronous_batch.add(*args)
172
- end
173
-
174
- def execute(options=nil)
175
- synchronous_backtrace { @asynchronous_batch.execute(options).value }
176
- end
177
- end
178
-
179
- # @private
180
- class AsynchronousPreparedStatementBatch < PreparedStatementBatch
181
- def initialize(prepared_statement, batch)
182
- @prepared_statement = prepared_statement
183
- @batch = batch
184
- end
185
-
186
- def add(*args)
187
- @batch.add(@prepared_statement, *args)
188
- end
189
-
190
- def execute(options=nil)
191
- @batch.execute(options)
192
- end
193
- end
194
-
195
- # @private
196
- class SynchronousPreparedStatementBatch < PreparedStatementBatch
197
- include SynchronousBacktrace
198
-
199
- def initialize(asynchronous_batch)
200
- @asynchronous_batch = asynchronous_batch
201
- end
202
-
203
- def add(*args)
204
- @asynchronous_batch.add(*args)
205
- end
206
-
207
- def execute(options=nil)
208
- synchronous_backtrace { @asynchronous_batch.execute(options).value }
209
- end
210
- end
211
- end
212
- end
@@ -1,591 +0,0 @@
1
- # encoding: utf-8
2
-
3
- #--
4
- # Copyright 2013-2014 DataStax, Inc.
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #++
18
-
19
- module Cassandra
20
- module Client
21
- # A CQL client manages connections to one or more Cassandra nodes and you use
22
- # it run queries, insert and update data, prepare statements and switch
23
- # keyspaces.
24
- #
25
- # To get a reference to a client you call {Cassandra::Client.connect}. When you
26
- # don't need the client anymore you can call {#close} to close all connections.
27
- #
28
- # Internally the client runs an IO reactor in a background thread. The reactor
29
- # handles all IO and manages the connections to the Cassandra nodes. This
30
- # makes it possible for the client to handle highly concurrent applications
31
- # very efficiently.
32
- #
33
- # Client instances are threadsafe and you only need a single instance for in
34
- # an application. Using multiple instances is more likely to lead to worse
35
- # performance than better.
36
- #
37
- # Because the client opens sockets, and runs threads it cannot be used by
38
- # the child created when forking a process. If your application forks (for
39
- # example applications running in the Unicorn application server or Resque
40
- # task queue) you _must_ connect after forking.
41
- #
42
- # @see Cassandra::Client.connect
43
- class Client
44
- # @!method close
45
- #
46
- # Disconnect from all nodes.
47
- #
48
- # @return [Cassandra::Client]
49
-
50
- # @!method connected?
51
- #
52
- # Returns whether or not the client is connected.
53
- #
54
- # @return [true, false]
55
-
56
- # @!method keyspace
57
- #
58
- # Returns the name of the current keyspace, or `nil` if no keyspace has been
59
- # set yet.
60
- #
61
- # @return [String]
62
-
63
- # @!method use(keyspace)
64
- #
65
- # Changes keyspace by sending a `USE` statement to all connections.
66
- #
67
- # The the second parameter is meant for internal use only.
68
- #
69
- # @param [String] keyspace
70
- # @raise [Cassandra::Errors::ClientError] raised when the client is not connected
71
- # @return [nil]
72
-
73
- # @!method execute(cql, *values, options={})
74
- #
75
- # Execute a CQL statement, optionally passing bound values.
76
- #
77
- # When passing bound values the request encoder will have to guess what
78
- # types to encode the values as. For most types this will be no problem,
79
- # but for integers and floating point numbers the larger size will be
80
- # chosen (e.g. `BIGINT` and `DOUBLE` and not `INT` and `FLOAT`). You can
81
- # override the guessing with the `:type_hint` option. Don't use on-the-fly
82
- # bound values when you will issue the request multiple times, prepared
83
- # statements are almost always a better choice.
84
- #
85
- # _Please note that on-the-fly bound values are only supported by Cassandra
86
- # 2.0 and above._
87
- #
88
- # @example A simple CQL query
89
- # result = client.execute("SELECT * FROM users WHERE user_name = 'sue'")
90
- # result.each do |row|
91
- # p row
92
- # end
93
- #
94
- # @example Using on-the-fly bound values
95
- # client.execute('INSERT INTO users (user_name, full_name) VALUES (?, ?)', 'sue', 'Sue Smith')
96
- #
97
- # @example Using on-the-fly bound values with type hints
98
- # client.execute('INSERT INTO users (user_name, age) VALUES (?, ?)', 'sue', 33, type_hints: [nil, :int])
99
- #
100
- # @example Specifying the consistency as a symbol
101
- # client.execute("UPDATE users SET full_name = 'Sue S. Smith' WHERE user_name = 'sue'", consistency: :one)
102
- #
103
- # @example Specifying the consistency and other options
104
- # client.execute("SELECT * FROM users", consistency: :all, timeout: 1.5)
105
- #
106
- # @example Loading a big result page by page
107
- # result_page = client.execute("SELECT * FROM large_table WHERE id = 'partition_with_lots_of_data'", page_size: 100)
108
- # while result_page
109
- # result_page.each do |row|
110
- # p row
111
- # end
112
- # result_page = result_page.next_page
113
- # end
114
- #
115
- # @example Activating tracing for a query
116
- # result = client.execute("SELECT * FROM users", tracing: true)
117
- # p result.trace_id
118
- #
119
- # @param [String] cql
120
- # @param [Array] values Values to bind to any binding markers in the
121
- # query (i.e. "?" placeholders) -- using this feature is similar to
122
- # using a prepared statement, but without the type checking. The client
123
- # needs to guess which data types to encode the values as, and will err
124
- # on the side of caution, using types like BIGINT instead of INT for
125
- # integers, and DOUBLE instead of FLOAT for floating point numbers. It
126
- # is not recommended to use this feature for anything but convenience,
127
- # and the algorithm used to guess types is to be considered experimental.
128
- # @param [Hash] options
129
- # @option options [Symbol] :consistency (:quorum) The
130
- # consistency to use for this query.
131
- # @option options [Symbol] :serial_consistency (nil) The
132
- # consistency to use for conditional updates (`:serial` or
133
- # `:local_serial`), see the CQL documentation for the semantics of
134
- # serial consistencies and conditional updates. The default is assumed
135
- # to be `:serial` by the server if none is specified. Ignored for non-
136
- # conditional queries.
137
- # @option options [Integer] :timeout (nil) How long to wait
138
- # for a response. If this timeout expires a {Cassandra::Errors::TimeoutError} will
139
- # be raised.
140
- # @option options [Boolean] :trace (false) Request tracing
141
- # for this request. See {Cassandra::Client::QueryResult} and
142
- # {Cassandra::Client::VoidResult} for how to retrieve the tracing data.
143
- # @option options [Integer] :page_size (nil) Instead of
144
- # returning all rows, return the response in pages of this size. The
145
- # first result will contain the first page, to load subsequent pages
146
- # use {Cassandra::Client::QueryResult#next_page}.
147
- # @option options [Array] :type_hints (nil) When passing
148
- # on-the-fly bound values the request encoder will have to guess what
149
- # types to encode the values as. Using this option you can give it hints
150
- # and avoid it guessing wrong. The hints must be an array that has the
151
- # same number of arguments as the number of bound values, and each
152
- # element should be the type of the corresponding value, or nil if you
153
- # prefer the encoder to guess. The types should be provided as lower
154
- # case symbols, e.g. `:int`, `:time_uuid`, etc.
155
- # @raise [Cassandra::Errors::ClientError] raised when the client is not connected
156
- # @raise [Cassandra::Errors::TimeoutError] raised when a timeout was specified and no
157
- # response was received within the timeout.
158
- # @raise [Cassandra::Errors::ExecutionError] raised when the CQL has syntax errors or for
159
- # other situations when the server complains.
160
- # @return [nil, Cassandra::Client::QueryResult, Cassandra::Client::VoidResult] Some
161
- # queries have no result and return `nil`, but `SELECT` statements
162
- # return an `Enumerable` of rows (see {Cassandra::Client::QueryResult}), and
163
- # `INSERT` and `UPDATE` return a similar type
164
- # (see {Cassandra::Client::VoidResult}).
165
-
166
- # @!method prepare(cql)
167
- #
168
- # Returns a prepared statement that can be run over and over again with
169
- # different bound values.
170
- #
171
- # @see Cassandra::Client::PreparedStatement
172
- # @param [String] cql The CQL to prepare
173
- # @raise [Cassandra::Errors::ClientError] raised when the client is not connected
174
- # @raise [Cassandra::Errors::IoError] raised when there is an IO error, for example
175
- # if the server suddenly closes the connection
176
- # @raise [Cassandra::Errors::ExecutionError] raised when there is an error on the server
177
- # side, for example when you specify a malformed CQL query
178
- # @return [Cassandra::Client::PreparedStatement] an object encapsulating the
179
- # prepared statement
180
-
181
- # @!method batch(type=:logged, options={})
182
- #
183
- # Yields a batch when called with a block. The batch is automatically
184
- # executed at the end of the block and the result is returned.
185
- #
186
- # Returns a batch when called wihtout a block. The batch will remember
187
- # the options given and merge these with any additional options given
188
- # when {Cassandra::Client::Batch#execute} is called.
189
- #
190
- # Please note that the batch object returned by this method _is not thread
191
- # safe_.
192
- #
193
- # The type parameter can be ommitted and the options can then be given
194
- # as first parameter.
195
- #
196
- # @example Executing queries in a batch
197
- # client.batch do |batch|
198
- # batch.add(%(INSERT INTO metrics (id, time, value) VALUES (1234, NOW(), 23423)))
199
- # batch.add(%(INSERT INTO metrics (id, time, value) VALUES (2346, NOW(), 13)))
200
- # batch.add(%(INSERT INTO metrics (id, time, value) VALUES (2342, NOW(), 2367)))
201
- # batch.add(%(INSERT INTO metrics (id, time, value) VALUES (4562, NOW(), 1231)))
202
- # end
203
- #
204
- # @example Using the returned batch object
205
- # batch = client.batch(:counter, trace: true)
206
- # batch.add('UPDATE counts SET value = value + ? WHERE id = ?', 4, 87654)
207
- # batch.add('UPDATE counts SET value = value + ? WHERE id = ?', 3, 6572)
208
- # result = batch.execute(timeout: 10)
209
- # puts result.trace_id
210
- #
211
- # @example Providing type hints for on-the-fly bound values
212
- # batch = client.batch
213
- # batch.add('UPDATE counts SET value = value + ? WHERE id = ?', 4, type_hints: [:int])
214
- # batch.execute
215
- #
216
- # @see Cassandra::Client::Batch
217
- # @param [Symbol] type the type of batch, must be one of `:logged`,
218
- # `:unlogged` and `:counter`. The precise meaning of these is defined
219
- # in the CQL specification.
220
- # @yieldparam [Cassandra::Client::Batch] batch the batch
221
- # @return [Cassandra::Client::VoidResult, Cassandra::Client::Batch] when no block is
222
- # given the batch is returned, when a block is given the result of
223
- # executing the batch is returned (see {Cassandra::Client::Batch#execute}).
224
- end
225
-
226
- # @private
227
- class AsynchronousClient < Client
228
- def initialize(options={})
229
- @compressor = options[:compressor]
230
- @cql_version = options[:cql_version]
231
- @logger = options[:logger] || NullLogger.new
232
- @protocol_version = options[:protocol_version] || 2
233
- @io_reactor = options[:io_reactor] || Ione::Io::IoReactor.new
234
- @hosts = extract_hosts(options)
235
- @initial_keyspace = options[:keyspace]
236
- @connections_per_node = options[:connections_per_node] || 1
237
- @lock = Mutex.new
238
- @request_runner = RequestRunner.new
239
- @connection_manager = ConnectionManager.new
240
- @execute_options_decoder = ExecuteOptionsDecoder.new(options[:default_consistency] || DEFAULT_CONSISTENCY)
241
- @port = options[:port] || DEFAULT_PORT
242
- @connection_timeout = options[:connection_timeout] || DEFAULT_CONNECTION_TIMEOUT
243
- @credentials = options[:credentials]
244
- @auth_provider = options[:auth_provider] || @credentials && Auth::Providers::Password.new(*@credentials.values_at(:username, :password))
245
- @connected = false
246
- @connecting = false
247
- @closing = false
248
- end
249
-
250
- def connect
251
- @lock.synchronize do
252
- raise Errors::ClientError, 'Cannot connect a closed client' if @closing || @closed
253
- return @connected_future if can_execute?
254
- @connecting = true
255
- @connected_future = begin
256
- f = @io_reactor.start
257
- f = f.flat_map { connect_with_protocol_version_fallback }
258
- f = f.flat_map { |connections| connect_to_all_peers(connections) }
259
- f = f.flat_map do |connections|
260
- @connection_manager.add_connections(connections)
261
- register_event_listener(@connection_manager.random_connection)
262
- end
263
- f = f.flat_map { use_keyspace(@connection_manager.snapshot, @initial_keyspace) }
264
- f.map(self)
265
- end
266
- end
267
- @connected_future.on_complete(&method(:connected))
268
- @connected_future
269
- end
270
-
271
- def close
272
- @lock.synchronize do
273
- return @closed_future if @closing
274
- @closing = true
275
- @closed_future = begin
276
- if @connecting
277
- f = @connected_future.recover
278
- f = f.flat_map { @io_reactor.stop }
279
- f = f.map(self)
280
- f
281
- else
282
- f = @io_reactor.stop
283
- f = f.map(self)
284
- f
285
- end
286
- end
287
- end
288
- @closed_future.on_complete(&method(:closed))
289
- @closed_future
290
- end
291
-
292
- def connected?
293
- @connected
294
- end
295
-
296
- def keyspace
297
- @connection_manager.random_connection.keyspace
298
- end
299
-
300
- def use(keyspace)
301
- with_failure_handler do
302
- connections = @connection_manager.reject { |c| c.keyspace == keyspace }
303
- return Ione::Future.resolved if connections.empty?
304
- use_keyspace(connections, keyspace).map(nil)
305
- end
306
- end
307
-
308
- def execute(cql, *args)
309
- with_failure_handler do
310
- options_or_consistency = nil
311
- if args.last.is_a?(Symbol) || args.last.is_a?(Hash)
312
- options_or_consistency = args.pop
313
- end
314
- options = @execute_options_decoder.decode_options(options_or_consistency)
315
- request = Protocol::QueryRequest.new(cql, args, options[:type_hints], options[:consistency], options[:serial_consistency], options[:page_size], options[:paging_state], options[:trace])
316
- f = execute_request(request, options[:timeout])
317
- if options.include?(:page_size)
318
- f = f.map { |result| AsynchronousQueryPagedQueryResult.new(self, request, result, options) }
319
- end
320
- f
321
- end
322
- end
323
-
324
- def prepare(cql)
325
- with_failure_handler do
326
- AsynchronousPreparedStatement.prepare(cql, @execute_options_decoder, @connection_manager, @logger)
327
- end
328
- end
329
-
330
- def batch(type=:logged, options=nil)
331
- if type.is_a?(Hash)
332
- options = type
333
- type = :logged
334
- end
335
- b = AsynchronousBatch.new(type, @execute_options_decoder, @connection_manager, options)
336
- if block_given?
337
- yield b
338
- b.execute
339
- else
340
- b
341
- end
342
- end
343
-
344
- private
345
-
346
- DEFAULT_CQL_VERSIONS = {1 => '3.0.0'}
347
- DEFAULT_CQL_VERSIONS.default = '3.1.0'
348
- DEFAULT_CQL_VERSIONS.freeze
349
- DEFAULT_CONSISTENCY = :quorum
350
- DEFAULT_PORT = 9042
351
- DEFAULT_CONNECTION_TIMEOUT = 10
352
- MAX_RECONNECTION_ATTEMPTS = 5
353
-
354
- def extract_hosts(options)
355
- if options[:hosts] && options[:hosts].any?
356
- options[:hosts].uniq
357
- elsif options[:host]
358
- options[:host].split(',').uniq
359
- else
360
- %w[localhost]
361
- end
362
- end
363
-
364
- def create_cluster_connector
365
- cql_version = @cql_version || DEFAULT_CQL_VERSIONS[@protocol_version]
366
- authentication_step = @protocol_version == 1 ? CredentialsAuthenticationStep.new(@credentials) : SaslAuthenticationStep.new(@auth_provider)
367
- protocol_handler_factory = lambda { |connection| Protocol::CqlProtocolHandler.new(connection, @io_reactor, @protocol_version, @compressor) }
368
- ClusterConnector.new(
369
- Connector.new([
370
- ConnectStep.new(@io_reactor, protocol_handler_factory, @port, @connection_timeout, @logger),
371
- CacheOptionsStep.new,
372
- InitializeStep.new(@compressor, @logger),
373
- authentication_step,
374
- CachePropertiesStep.new,
375
- ]),
376
- @logger
377
- )
378
- end
379
-
380
- def connect_with_protocol_version_fallback
381
- f = create_cluster_connector.connect_all(@hosts, @connections_per_node)
382
- f.fallback do |error|
383
- if error.is_a?(Errors::ProtocolError) && @protocol_version > 1
384
- @logger.warn('Could not connect using protocol version %d (will try again with %d): %s' % [@protocol_version, @protocol_version - 1, error.message])
385
- @protocol_version -= 1
386
- connect_with_protocol_version_fallback
387
- else
388
- raise(error, error.message, error.backtrace)
389
- end
390
- end
391
- end
392
-
393
- def connect_to_all_peers(seed_connections, initial_keyspace=@initial_keyspace)
394
- @logger.debug('Looking for additional nodes')
395
- peer_discovery = PeerDiscovery.new(seed_connections)
396
- peer_discovery.new_hosts.flat_map do |hosts|
397
- if hosts.empty?
398
- @logger.debug('No additional nodes found')
399
- Ione::Future.resolved(seed_connections)
400
- else
401
- @logger.debug('%d additional nodes found' % hosts.size)
402
- f = create_cluster_connector.connect_all(hosts, @connections_per_node)
403
- f = f.map do |discovered_connections|
404
- seed_connections + discovered_connections
405
- end
406
- f.recover(seed_connections)
407
- end
408
- end
409
- end
410
-
411
- def connected(f)
412
- if f.resolved?
413
- @lock.synchronize do
414
- @connecting = false
415
- @connected = true
416
- end
417
- @logger.info('Cluster connection complete')
418
- else
419
- @lock.synchronize do
420
- @connecting = false
421
- @connected = false
422
- end
423
- f.on_failure do |e|
424
- @logger.error('Failed connecting to cluster: %s' % e.message)
425
- end
426
- close
427
- end
428
- end
429
-
430
- def closed(f)
431
- @lock.synchronize do
432
- @closing = false
433
- @closed = true
434
- @connected = false
435
- if f.resolved?
436
- @logger.info('Cluster disconnect complete')
437
- else
438
- f.on_failure do |e|
439
- @logger.error('Cluster disconnect failed: %s' % e.message)
440
- end
441
- end
442
- end
443
- end
444
-
445
- def can_execute?
446
- !@closing && (@connecting || (@connected && @connection_manager.connected?))
447
- end
448
-
449
- def with_failure_handler
450
- return Ione::Future.failed(Errors::ClientError.new) unless can_execute?
451
- yield
452
- rescue => e
453
- Ione::Future.failed(e)
454
- end
455
-
456
- def use_keyspace(connections, keyspace)
457
- return Ione::Future.resolved(connections) unless keyspace
458
- return Ione::Future.failed(InvalidKeyspaceNameError.new(%("#{keyspace}" is not a valid keyspace name))) unless keyspace =~ /^\w[\w\d_]*$|^"\w[\w\d_]*"$/
459
-
460
- futures = connections.map do |connection|
461
- request = Protocol::QueryRequest.new("USE #{keyspace}", nil, nil, :one)
462
- @request_runner.execute(connection, request).map(connection)
463
- end
464
- Ione::Future.all(*futures)
465
- end
466
-
467
- def register_event_listener(connection)
468
- register_request = Protocol::RegisterRequest.new(Protocol::TopologyChangeEventResponse::TYPE, Protocol::StatusChangeEventResponse::TYPE)
469
- f = execute_request(register_request, nil, connection)
470
- f.on_value do
471
- connection.on_closed do
472
- if connected?
473
- begin
474
- register_event_listener(@connection_manager.random_connection)
475
- rescue Errors::IOError
476
- # we had started closing down after the connection check
477
- end
478
- end
479
- end
480
- connection.on_event do |event|
481
- if event.change == 'UP' || event.change == 'NEW_NODE'
482
- @logger.debug('Received %s event' % event.change)
483
- unless @looking_for_nodes
484
- @looking_for_nodes = true
485
- handle_topology_change.on_complete do |f|
486
- @looking_for_nodes = false
487
- end
488
- end
489
- end
490
- end
491
- end
492
- f
493
- end
494
-
495
- def handle_topology_change(remaning_attempts=MAX_RECONNECTION_ATTEMPTS)
496
- with_failure_handler do
497
- seed_connections = @connection_manager.snapshot
498
- f = connect_to_all_peers(seed_connections, keyspace)
499
- f.flat_map do |all_connections|
500
- new_connections = all_connections - seed_connections
501
- if new_connections.size > 0
502
- f = use_keyspace(new_connections, keyspace)
503
- f.on_value do
504
- @connection_manager.add_connections(new_connections)
505
- end
506
- f
507
- elsif remaning_attempts > 0
508
- timeout = 2**(MAX_RECONNECTION_ATTEMPTS - remaning_attempts)
509
- @logger.debug('Scheduling new peer discovery in %ds' % timeout)
510
- f = @io_reactor.schedule_timer(timeout)
511
- f.flat_map do
512
- handle_topology_change(remaning_attempts - 1)
513
- end
514
- else
515
- @logger.warn('Giving up looking for additional nodes')
516
- Ione::Future.resolved
517
- end
518
- end
519
- end
520
- end
521
-
522
- def execute_request(request, timeout=nil, connection=nil)
523
- f = @request_runner.execute(connection || @connection_manager.random_connection, request, timeout)
524
- f.map do |result|
525
- if result.is_a?(KeyspaceChanged)
526
- use(result.keyspace)
527
- nil
528
- else
529
- result
530
- end
531
- end
532
- end
533
- end
534
-
535
- # @private
536
- class SynchronousClient < Client
537
- include SynchronousBacktrace
538
-
539
- def initialize(async_client)
540
- @async_client = async_client
541
- end
542
-
543
- def connect
544
- synchronous_backtrace { @async_client.connect.value }
545
- self
546
- end
547
-
548
- def close
549
- synchronous_backtrace { @async_client.close.value }
550
- self
551
- end
552
-
553
- def connected?
554
- @async_client.connected?
555
- end
556
-
557
- def keyspace
558
- @async_client.keyspace
559
- end
560
-
561
- def use(keyspace)
562
- synchronous_backtrace { @async_client.use(keyspace).value }
563
- end
564
-
565
- def execute(cql, *args)
566
- synchronous_backtrace do
567
- result = @async_client.execute(cql, *args).value
568
- result = SynchronousPagedQueryResult.new(result) if result.is_a?(PagedQueryResult)
569
- result
570
- end
571
- end
572
-
573
- def prepare(cql)
574
- async_statement = synchronous_backtrace { @async_client.prepare(cql).value }
575
- SynchronousPreparedStatement.new(async_statement)
576
- end
577
-
578
- def batch(type=:logged, options={}, &block)
579
- if block_given?
580
- synchronous_backtrace { @async_client.batch(type, options, &block).value }
581
- else
582
- SynchronousBatch.new(@async_client.batch(type, options))
583
- end
584
- end
585
-
586
- def async
587
- @async_client
588
- end
589
- end
590
- end
591
- end