corm 0.0.22 → 0.0.23

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ # encoding: utf-8
2
+
3
+ module Corm
4
+ module Enhancements
5
+
6
+ def enhancements_logger(logger = nil)
7
+ Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } unless logger
8
+ end
9
+
10
+ RESCUED_CASSANDRA_EXCEPTIONS = [
11
+ ::Cassandra::Errors::ExecutionError,
12
+ ::Cassandra::Errors::IOError,
13
+ ::Cassandra::Errors::InternalError,
14
+ ::Cassandra::Errors::NoHostsAvailable,
15
+ ::Cassandra::Errors::ServerError,
16
+ ::Cassandra::Errors::TimeoutError
17
+ ]
18
+
19
+ # Trying to rescue from a Cassandra::Error
20
+ #
21
+ # The relevant documentation is here (version 2.1.3):
22
+ # https://datastax.github.io/ruby-driver/api/error/
23
+ #
24
+ # Saving from:
25
+ #
26
+ # - ::Cassandra::Errors::ExecutionError
27
+ # - ::Cassandra::Errors::IOError
28
+ # - ::Cassandra::Errors::InternalError
29
+ # - ::Cassandra::Errors::NoHostsAvailable
30
+ # - ::Cassandra::Errors::ServerError
31
+ # - ::Cassandra::Errors::TimeoutError
32
+ #
33
+ # Ignoring:
34
+ # - Errors::ClientError
35
+ # - Errors::DecodingError
36
+ # - Errors::EncodingError
37
+ # - Errors::ValidationError
38
+ #
39
+ # A possible and maybe-good refactoring could be refine for the
40
+ # network related issues.
41
+ def attempts_wrapper(attempts = 3, &block)
42
+ (1..attempts).each do |i|
43
+ begin
44
+ return block.call() if block_given?
45
+ rescue *RESCUED_CASSANDRA_EXCEPTIONS => e
46
+ sleep_for = i * Random.rand(1.5..2.5)
47
+ enhancements_logger.error { "(#{i}/#{attempts} attempts) Bad fail! Retry in #{sleep_for} seconds to recover #{e.class.name}: #{e.message}" }
48
+ sleep(sleep_for)
49
+ end
50
+ end
51
+ nil
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ module Corm
4
+ class TooManyKeysError < StandardError; end
5
+ class UnknownKey < StandardError; end
6
+ class MissingPartitionKey < StandardError; end
7
+ class MissingClusteringKey < StandardError; end
8
+ class UnknownClusteringKey < StandardError; end
9
+ end
@@ -1,25 +1,43 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'cassandra'
4
+ require 'corm/enhancements'
5
+ require 'corm/exceptions'
6
+ require 'corm/validations'
4
7
  require 'multi_json'
5
8
  require 'set'
9
+ require 'digest/md5'
6
10
 
7
11
  module Corm
8
12
  class Model
9
13
  include Enumerable
14
+ extend Enhancements
15
+ extend Validations
10
16
 
11
17
  @@cluster = nil
12
18
 
19
+ # Since the `Cassandra.cluster` method wants to connect, the configure will
20
+ # retry a couple of times (implemented in the Enhancements module) before
21
+ # give up...
22
+ # If it fails `@@cluster` was nil and nil remains.
13
23
  def self.configure(opts = {})
14
- @@cluster = Cassandra.cluster(opts)
24
+ attempts_wrapper do
25
+ @@cluster = Cassandra.cluster(opts)
26
+ end
15
27
  end
16
28
 
17
29
  def self.cluster
18
30
  @@cluster
19
31
  end
20
32
 
33
+ # Please, note the wrapper around the `session execute`.
34
+ # This wrapper (implemented in the Enhancements module) will recover some
35
+ # Cassandra::Error(s) retrying a couple of times.
36
+ #
37
+ # Note also that the <instance>#execute is just calling this Class#execute,
38
+ # as well as count, find, get, drop, etc...
21
39
  def self.execute(*args)
22
- session.execute(*args)
40
+ attempts_wrapper { session.execute(*args) }
23
41
  end
24
42
 
25
43
  def self.field(name, type, pkey = false)
@@ -139,20 +157,74 @@ module Corm
139
157
  end
140
158
 
141
159
  def self.drop!
142
- execute("DROP TABLE #{[keyspace, table].compact.join '.'};")
160
+ execute("DROP TABLE IF EXISTS #{[keyspace, table].compact.join('.')};")
143
161
  end
144
162
 
145
- def self.get(relations)
146
- if statements['get'].nil?
147
- fields = primary_key.flatten.map { |key| "#{key} = ?" }.join ' AND '
148
- statement = "SELECT * FROM #{keyspace}.#{table} WHERE #{fields} LIMIT 1;"
149
- statements['get'] = session.prepare statement
163
+ ##
164
+ # Find by keys.
165
+ # This `find` methods wants to be as flexible as possible.
166
+ #
167
+ # Unless a block is given, it returns an `Enumerator`, otherwise it yields
168
+ # to the block an instance of the found Cassandra entries.
169
+ #
170
+ # If no keys is passed as parameter, the methods returns (an Enumerator for)
171
+ # all the results in the table.
172
+ #
173
+ # The options hash support the ':limit' option to append at the statement;
174
+ # the default is no limit.
175
+ #
176
+ # The 'key_values' parameter is an Hash where the keys are the "column
177
+ # names" and the values... are the values.
178
+ #
179
+ # If the keys passed as parameter are more than the defined by the table the
180
+ # query is not valid, it cannot be executed and an error is raised.
181
+ # Other exceptions are raised when the keys doesn't include all the
182
+ # (required) partition_keys or the clustering keys doesn't are in the
183
+ # defined order.
184
+ def self.find(key_values = {}, query_options = {}, &block)
185
+
186
+ raise ArgumentError unless key_values.is_a?(Hash)
187
+ raise ArgumentError unless query_options.is_a?(Hash)
188
+
189
+ unless key_values.empty?
190
+ raise TooManyKeysError if there_are_too_many_keys_requested?(key_values)
191
+ raise MissingPartitionKey if a_partition_key_is_missing?(key_values)
192
+ raise UnknownClusteringKey if an_unknown_clustering_key_is_requested?(key_values)
193
+ # raise UnknownKey if an_unknown_key_is_requested?(key_values)
194
+ raise MissingClusteringKey if a_clustering_key_is_missing?(key_values)
150
195
  end
151
- values = primary_key.flatten.map do |key|
152
- relations[key.to_s] || relations[key.to_sym]
196
+
197
+ return to_enum(:find, key_values, query_options) unless block_given?
198
+
199
+ statement_find_key = Array(query_options.fetch(:statement_key, 'find')).flatten
200
+ field_names = []
201
+
202
+ key_values.each do |key, value|
203
+
204
+ statement_find_key << key.to_s
205
+ field_names << "#{key} = ?"
153
206
  end
154
- cassandra_record_ = execute(statements['get'], arguments: values).first
155
- cassandra_record_ ? new(_cassandra_record: cassandra_record_) : nil
207
+
208
+ statement_find_key = statement_find_key.join('_')
209
+ statement_find_key.concat("_limit#{query_options[:limit]}") if query_options[:limit]
210
+
211
+ if statements[statement_find_key].nil?
212
+ statement = self.the_select_statement_for(key_values, field_names, query_options[:limit])
213
+ statements[statement_find_key] = session.prepare(statement)
214
+ end
215
+
216
+ execute(statements[statement_find_key], arguments: key_values).each do |cassandra_record_|
217
+ block.call(new(_cassandra_record: cassandra_record_))
218
+ end
219
+ end
220
+
221
+ def self.get(relations)
222
+ query_options = {
223
+ limit: 1,
224
+ statement_key: 'get'
225
+ }
226
+ cassandra_record = self.find(relations, query_options).first
227
+ return cassandra_record ? cassandra_record : nil
156
228
  end
157
229
 
158
230
  def self.keyspace(name = nil)
@@ -160,14 +232,20 @@ module Corm
160
232
  class_variable_get(:@@keyspace)
161
233
  end
162
234
 
235
+ # Eventually set and return the session, taken from the connection to
236
+ # the cluster.
237
+ #
238
+ # This operation is wrapped by the retry policy (module Enhancements).
163
239
  def self.keyspace!(opts = {})
164
240
  replication = opts[:replication] ||
165
241
  "{'class': 'SimpleStrategy', 'replication_factor': '1'}"
166
242
  durable_writes = opts[:durable_writes].nil? ? true : opts[:durable_writes]
167
243
  if_not_exists = opts[:if_not_exists] ? 'IF NOT EXISTS' : ''
168
- cluster.connect.execute(
169
- "CREATE KEYSPACE #{if_not_exists} #{keyspace} WITH replication = #{replication} AND durable_writes = #{durable_writes};"
170
- )
244
+ attempts_wrapper do
245
+ cluster.connect.execute(
246
+ "CREATE KEYSPACE #{if_not_exists} #{keyspace} WITH replication = #{replication} AND durable_writes = #{durable_writes};"
247
+ )
248
+ end
171
249
  end
172
250
 
173
251
  def self.primary_key(partition_key = nil, *cols)
@@ -178,6 +256,18 @@ module Corm
178
256
  class_variable_get(:@@primary_key)
179
257
  end
180
258
 
259
+ def self.primary_key_count
260
+ self.primary_key.flatten.count
261
+ end
262
+
263
+ def self.partition_key
264
+ self.primary_key.first.map(&:to_sym)
265
+ end
266
+
267
+ def self.clustering_key
268
+ self.primary_key.count == 2 ? self.primary_key[1].flatten.map(&:to_sym) : []
269
+ end
270
+
181
271
  def self.properties(*args)
182
272
  class_variable_set(
183
273
  :@@properties,
@@ -186,11 +276,19 @@ module Corm
186
276
  class_variable_get(:@@properties)
187
277
  end
188
278
 
279
+ # Eventually set and return the session, taken from the connection to
280
+ # the cluster.
281
+ #
282
+ # This operation is wrapped by the retry policy (module Enhancements).
189
283
  def self.session
190
- class_variable_set(
191
- :@@session,
192
- cluster.connect(keyspace)
193
- ) unless class_variable_defined?(:@@session)
284
+ unless class_variable_defined?(:@@session)
285
+ attempts_wrapper do
286
+ class_variable_set(
287
+ :@@session,
288
+ cluster.connect(keyspace)
289
+ )
290
+ end
291
+ end
194
292
  class_variable_get :@@session
195
293
  end
196
294
 
@@ -295,5 +393,22 @@ module Corm
295
393
  def table
296
394
  self.class.table
297
395
  end
396
+
397
+ ##
398
+ # Create and return the proper query to find the C* entries, given an array
399
+ # of keys.
400
+ #
401
+ # @param key_values An array of key names; can be empty, cannot be, in size, greater than the length of the model keys.
402
+ # @param field_names The "column names" for the `WHERE` clause.
403
+ def self.the_select_statement_for(key_values, field_names, limit = nil)
404
+ limit = "LIMIT #{limit.to_i}" if (limit && limit > 0)
405
+ if key_values.empty?
406
+ return "SELECT * FROM #{keyspace}.#{table} #{limit} ;"
407
+ elsif key_values.count > self.primary_key_count
408
+ raise Corm::TooManyKeysError
409
+ else
410
+ return "SELECT * FROM #{keyspace}.#{table} WHERE #{field_names.join(' AND ')} #{limit} ;"
411
+ end
412
+ end
298
413
  end
299
414
  end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ module Corm
4
+ module Validations
5
+
6
+ def there_are_too_many_keys_requested?(key_values)
7
+ if key_values.keys.count > self.primary_key_count
8
+ return "You defined more find keys than the primary keys of the table!"
9
+ end
10
+ false
11
+ end
12
+
13
+ def an_unknown_key_is_requested?(key_values)
14
+ unless (key_values.keys - self.primary_key.flatten).empty?
15
+ return "You requested a key that it's not in the table primary key!"
16
+ end
17
+ false
18
+ end
19
+
20
+ def an_unknown_clustering_key_is_requested?(key_values)
21
+ unless ((key_values.keys - self.partition_key.flatten) - self.clustering_key).empty?
22
+ return "You requested some unsupported clustering keys!"
23
+ end
24
+ false
25
+ end
26
+
27
+ def a_partition_key_is_missing?(key_values)
28
+ self.partition_key.each do |part_key|
29
+ return "#{part_key} is required as partition key!" unless key_values.keys.include?(part_key.to_sym)
30
+ end
31
+ false
32
+ end
33
+
34
+ def a_clustering_key_is_missing?(key_values)
35
+
36
+ # This exception mimic the following...
37
+ # Class: <Cassandra::Errors::InvalidError>
38
+ # Message: <"PRIMARY KEY column 'still_another_uuid_field' cannot be
39
+ # restricted (preceding column 'another_uuid_field' is either not
40
+ # restricted or by a non-EQ relation)"
41
+ #
42
+ # ... and TBH leaving this check to the Cassandra driver is still an
43
+ # option.
44
+ return false if self.clustering_key.empty?
45
+ return false if no_clustering_key_requested?(key_values)
46
+ self.clustering_key.each do |clust_key|
47
+ return "#{clust_key} is required as clustering key! (Order matters)" unless key_values.include?(clust_key.to_sym)
48
+ end
49
+ false
50
+ end
51
+
52
+ def no_clustering_key_requested?(key_values)
53
+ (key_values.keys - self.partition_key.flatten).empty?
54
+ end
55
+ end
56
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: corm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.22
4
+ version: 0.0.23
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-06-11 00:00:00.000000000 Z
12
+ date: 2015-06-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cassandra-driver
@@ -71,6 +71,22 @@ dependencies:
71
71
  - - ! '>='
72
72
  - !ruby/object:Gem::Version
73
73
  version: 10.0.0
74
+ - !ruby/object:Gem::Dependency
75
+ name: pry
76
+ requirement: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: 0.10.1
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.10.1
74
90
  description: ''
75
91
  email:
76
92
  - stefano@gild.com
@@ -79,7 +95,10 @@ extensions: []
79
95
  extra_rdoc_files: []
80
96
  files:
81
97
  - lib/corm.rb
98
+ - lib/corm/enhancements.rb
99
+ - lib/corm/exceptions.rb
82
100
  - lib/corm/model.rb
101
+ - lib/corm/validations.rb
83
102
  homepage: https://github.com/stefanofontanelli/corm
84
103
  licenses: []
85
104
  post_install_message:
@@ -92,12 +111,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
111
  - - ! '>='
93
112
  - !ruby/object:Gem::Version
94
113
  version: '0'
114
+ segments:
115
+ - 0
116
+ hash: 1645116542226153215
95
117
  required_rubygems_version: !ruby/object:Gem::Requirement
96
118
  none: false
97
119
  requirements:
98
120
  - - ! '>='
99
121
  - !ruby/object:Gem::Version
100
122
  version: '0'
123
+ segments:
124
+ - 0
125
+ hash: 1645116542226153215
101
126
  requirements: []
102
127
  rubyforge_project:
103
128
  rubygems_version: 1.8.23.2