corm 0.0.25 → 0.0.26
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/corm.rb +1 -0
- data/lib/corm/exceptions.rb +6 -5
- data/lib/corm/model.rb +43 -83
- data/lib/corm/retry/policies.rb +1 -0
- data/lib/corm/retry/policies/default.rb +30 -0
- data/lib/corm/validations.rb +36 -39
- metadata +6 -5
- data/lib/corm/enhancements.rb +0 -54
data/lib/corm.rb
CHANGED
data/lib/corm/exceptions.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Corm
|
4
|
-
class
|
5
|
-
class
|
6
|
-
class
|
7
|
-
class
|
8
|
-
class
|
4
|
+
class GenericError < StandardError; end
|
5
|
+
class TooManyKeysError < GenericError; end
|
6
|
+
class UnknownPrimaryKey < GenericError; end
|
7
|
+
class MissingPartitionKey < GenericError; end
|
8
|
+
class MissingClusteringKey < GenericError; end
|
9
|
+
class UnknownClusteringKey < GenericError; end
|
9
10
|
end
|
data/lib/corm/model.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
require 'cassandra'
|
4
|
-
require 'corm/enhancements'
|
5
4
|
require 'corm/exceptions'
|
6
5
|
require 'corm/validations'
|
7
6
|
require 'multi_json'
|
@@ -11,7 +10,6 @@ require 'digest/md5'
|
|
11
10
|
module Corm
|
12
11
|
class Model
|
13
12
|
include Enumerable
|
14
|
-
extend Enhancements
|
15
13
|
extend Validations
|
16
14
|
|
17
15
|
@@cluster = nil
|
@@ -21,23 +19,15 @@ module Corm
|
|
21
19
|
# give up...
|
22
20
|
# If it fails `@@cluster` was nil and nil remains.
|
23
21
|
def self.configure(opts = {})
|
24
|
-
|
25
|
-
@@cluster = Cassandra.cluster(opts)
|
26
|
-
end
|
22
|
+
@@cluster = Cassandra.cluster(opts)
|
27
23
|
end
|
28
24
|
|
29
25
|
def self.cluster
|
30
26
|
@@cluster
|
31
27
|
end
|
32
28
|
|
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...
|
39
29
|
def self.execute(*args)
|
40
|
-
|
30
|
+
session.execute(*args)
|
41
31
|
end
|
42
32
|
|
43
33
|
def self.field(name, type, pkey = false)
|
@@ -173,7 +163,7 @@ module Corm
|
|
173
163
|
# The options hash support the ':limit' option to append at the statement;
|
174
164
|
# the default is no limit.
|
175
165
|
#
|
176
|
-
# The '
|
166
|
+
# The 'params' parameter is an Hash where the keys are the "column
|
177
167
|
# names" and the values... are the values.
|
178
168
|
#
|
179
169
|
# If the keys passed as parameter are more than the defined by the table the
|
@@ -181,50 +171,27 @@ module Corm
|
|
181
171
|
# Other exceptions are raised when the keys doesn't include all the
|
182
172
|
# (required) partition_keys or the clustering keys doesn't are in the
|
183
173
|
# defined order.
|
184
|
-
def self.find(
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
end
|
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} = ?"
|
174
|
+
def self.find(params = {}, opts = {}, &block)
|
175
|
+
validate_query(params, opts)
|
176
|
+
return to_enum(:find, params, opts) unless block_given?
|
177
|
+
statement_key = Array(opts.fetch(:statement_key, 'find')).flatten
|
178
|
+
statement_key.concat(params.keys)
|
179
|
+
statement_key.concat(["_limit#{opts[:limit]}"]) if opts[:limit]
|
180
|
+
statement_key = statement_key.join('_')
|
181
|
+
fields = params.map { |key, _value| "#{key} = ?" }
|
182
|
+
if statements[statement_key].nil?
|
183
|
+
statement = select_statement_for(fields, opts[:limit])
|
184
|
+
statements[statement_key] = session.prepare(statement)
|
206
185
|
end
|
207
|
-
|
208
|
-
|
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_))
|
186
|
+
execute(statements[statement_key], arguments: params.values).each do |res|
|
187
|
+
block.call(new(_cassandra_record: res))
|
218
188
|
end
|
219
189
|
end
|
220
190
|
|
221
191
|
def self.get(relations)
|
222
|
-
query_options = {
|
223
|
-
|
224
|
-
|
225
|
-
}
|
226
|
-
cassandra_record = self.find(relations, query_options).first
|
227
|
-
return cassandra_record ? cassandra_record : nil
|
192
|
+
query_options = { limit: 1, statement_key: 'get' }
|
193
|
+
cassandra_record = find(relations, query_options).first
|
194
|
+
cassandra_record ? cassandra_record : nil
|
228
195
|
end
|
229
196
|
|
230
197
|
def self.keyspace(name = nil)
|
@@ -241,11 +208,9 @@ module Corm
|
|
241
208
|
"{'class': 'SimpleStrategy', 'replication_factor': '1'}"
|
242
209
|
durable_writes = opts[:durable_writes].nil? ? true : opts[:durable_writes]
|
243
210
|
if_not_exists = opts[:if_not_exists] ? 'IF NOT EXISTS' : ''
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
)
|
248
|
-
end
|
211
|
+
cluster.connect.execute(
|
212
|
+
"CREATE KEYSPACE #{if_not_exists} #{keyspace} WITH replication = #{replication} AND durable_writes = #{durable_writes};"
|
213
|
+
)
|
249
214
|
end
|
250
215
|
|
251
216
|
def self.primary_key(partition_key = nil, *cols)
|
@@ -257,15 +222,15 @@ module Corm
|
|
257
222
|
end
|
258
223
|
|
259
224
|
def self.primary_key_count
|
260
|
-
|
225
|
+
primary_key.flatten.count
|
261
226
|
end
|
262
227
|
|
263
228
|
def self.partition_key
|
264
|
-
|
229
|
+
primary_key.first.map(&:to_sym)
|
265
230
|
end
|
266
231
|
|
267
232
|
def self.clustering_key
|
268
|
-
|
233
|
+
primary_key.count == 2 ? primary_key[1].flatten.map(&:to_sym) : []
|
269
234
|
end
|
270
235
|
|
271
236
|
def self.properties(*args)
|
@@ -282,12 +247,10 @@ module Corm
|
|
282
247
|
# This operation is wrapped by the retry policy (module Enhancements).
|
283
248
|
def self.session
|
284
249
|
unless class_variable_defined?(:@@session)
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
)
|
290
|
-
end
|
250
|
+
class_variable_set(
|
251
|
+
:@@session,
|
252
|
+
cluster.connect(keyspace)
|
253
|
+
)
|
291
254
|
end
|
292
255
|
class_variable_get :@@session
|
293
256
|
end
|
@@ -366,6 +329,20 @@ module Corm
|
|
366
329
|
|
367
330
|
protected
|
368
331
|
|
332
|
+
##
|
333
|
+
# Create and return the proper query to find the C* entries, given an array
|
334
|
+
# of keys.
|
335
|
+
#
|
336
|
+
# @param key_values An array of key names; can be empty, cannot be, in size, greater than the length of the model keys.
|
337
|
+
# @param field_names The "column names" for the `WHERE` clause.
|
338
|
+
def self.select_statement_for(fields, limit = nil)
|
339
|
+
statement = "SELECT * FROM #{keyspace}.#{table}"
|
340
|
+
statement += " WHERE #{fields.join(' AND ')}" unless fields.empty?
|
341
|
+
statement += " LIMIT #{limit.to_i}" if limit.to_i > 0
|
342
|
+
statement += ';'
|
343
|
+
statement
|
344
|
+
end
|
345
|
+
|
369
346
|
def execute(*args)
|
370
347
|
self.class.execute(*args)
|
371
348
|
end
|
@@ -393,22 +370,5 @@ module Corm
|
|
393
370
|
def table
|
394
371
|
self.class.table
|
395
372
|
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
|
413
373
|
end
|
414
374
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'corm/retry/policies/default'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'cassandra'
|
3
|
+
|
4
|
+
module Corm
|
5
|
+
module Retry
|
6
|
+
module Policies
|
7
|
+
class Default
|
8
|
+
include Cassandra::Retry::Policy
|
9
|
+
|
10
|
+
def read_timeout(_statement, consistency, _required, _received, retrieved, retries)
|
11
|
+
return reraise if retries >= 5
|
12
|
+
sleep(retries.to_f + Random.rand(0.0..1.0))
|
13
|
+
retrieved ? reraise : try_again(consistency)
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_timeout(_statement, consistency, _type, _required, _received, retries)
|
17
|
+
return reraise if retries >= 5
|
18
|
+
sleep(retries.to_f + Random.rand(0.0..1.0))
|
19
|
+
try_again(consistency)
|
20
|
+
end
|
21
|
+
|
22
|
+
def unavailable(_statement, consistency, _required, _alive, retries)
|
23
|
+
return reraise if retries >= 5
|
24
|
+
sleep(retries.to_f + Random.rand(0.0..1.0))
|
25
|
+
try_again(consistency)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/corm/validations.rb
CHANGED
@@ -2,55 +2,52 @@
|
|
2
2
|
|
3
3
|
module Corm
|
4
4
|
module Validations
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
false
|
5
|
+
def clustering_key_missing?(params)
|
6
|
+
return false if no_clustering_key?(params)
|
7
|
+
keys = clustering_key.map(&:to_sym) - params.keys.map(&:to_sym)
|
8
|
+
keys.empty? ? false : true
|
11
9
|
end
|
12
10
|
|
13
|
-
def
|
14
|
-
|
15
|
-
return "You requested a key that it's not in the table primary key!"
|
16
|
-
end
|
17
|
-
false
|
11
|
+
def no_clustering_key?(params)
|
12
|
+
(params.keys.map(&:to_sym) - partition_key.flatten.map(&:to_sym)).empty?
|
18
13
|
end
|
19
14
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
end
|
24
|
-
false
|
15
|
+
def partition_key_missing?(params)
|
16
|
+
keys = partition_key.map(&:to_sym) - params.keys.map(&:to_sym)
|
17
|
+
keys.empty? ? false : true
|
25
18
|
end
|
26
19
|
|
27
|
-
def
|
28
|
-
|
29
|
-
return "#{part_key} is required as partition key!" unless key_values.keys.map(&:to_sym).include?(part_key.to_sym)
|
30
|
-
end
|
31
|
-
false
|
20
|
+
def too_many_keys?(params)
|
21
|
+
params.keys.count > primary_key_count ? true : false
|
32
22
|
end
|
33
23
|
|
34
|
-
def
|
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.keys.map(&:to_sym).include?(clust_key.to_sym)
|
48
|
-
end
|
49
|
-
false
|
24
|
+
def unknown_primary_key?(params)
|
25
|
+
(params.keys.map(&:to_sym) - primary_key.flatten.map(&:to_sym)).empty? ? false : true
|
50
26
|
end
|
51
27
|
|
52
|
-
def
|
53
|
-
|
28
|
+
def validate_query(params, opts)
|
29
|
+
if !params.is_a?(Hash)
|
30
|
+
error = ArgumentError
|
31
|
+
message = "'params' argument must be an hash: #{params}"
|
32
|
+
elsif !opts.is_a?(Hash)
|
33
|
+
error = ArgumentError
|
34
|
+
message = "'opts' argument must be an hash: #{opts}"
|
35
|
+
elsif too_many_keys?(params)
|
36
|
+
error = TooManyKeysError
|
37
|
+
message = "#{params.keys}"
|
38
|
+
elsif !params.empty? && partition_key_missing?(params)
|
39
|
+
error = MissingPartitionKey
|
40
|
+
message = "#{params.keys}"
|
41
|
+
elsif !params.empty? && clustering_key_missing?(params)
|
42
|
+
error = MissingClusteringKey
|
43
|
+
message = "#{params.keys}"
|
44
|
+
elsif !params.empty? && unknown_primary_key?(params)
|
45
|
+
error = UnknownPrimaryKey
|
46
|
+
message = "#{params.keys}"
|
47
|
+
else
|
48
|
+
return
|
49
|
+
end
|
50
|
+
fail(error, message, caller)
|
54
51
|
end
|
55
52
|
end
|
56
53
|
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.
|
4
|
+
version: 0.0.26
|
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-
|
12
|
+
date: 2015-06-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: cassandra-driver
|
@@ -95,9 +95,10 @@ extensions: []
|
|
95
95
|
extra_rdoc_files: []
|
96
96
|
files:
|
97
97
|
- lib/corm.rb
|
98
|
-
- lib/corm/enhancements.rb
|
99
98
|
- lib/corm/exceptions.rb
|
100
99
|
- lib/corm/model.rb
|
100
|
+
- lib/corm/retry/policies.rb
|
101
|
+
- lib/corm/retry/policies/default.rb
|
101
102
|
- lib/corm/validations.rb
|
102
103
|
homepage: https://github.com/stefanofontanelli/corm
|
103
104
|
licenses: []
|
@@ -113,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
113
114
|
version: '0'
|
114
115
|
segments:
|
115
116
|
- 0
|
116
|
-
hash: -
|
117
|
+
hash: -3769483612946445211
|
117
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
119
|
none: false
|
119
120
|
requirements:
|
@@ -122,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
123
|
version: '0'
|
123
124
|
segments:
|
124
125
|
- 0
|
125
|
-
hash: -
|
126
|
+
hash: -3769483612946445211
|
126
127
|
requirements: []
|
127
128
|
rubyforge_project:
|
128
129
|
rubygems_version: 1.8.23.2
|
data/lib/corm/enhancements.rb
DELETED
@@ -1,54 +0,0 @@
|
|
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
|