corm 0.0.22 → 0.0.23
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/corm/enhancements.rb +54 -0
- data/lib/corm/exceptions.rb +9 -0
- data/lib/corm/model.rb +134 -19
- data/lib/corm/validations.rb +56 -0
- metadata +27 -2
@@ -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
|
data/lib/corm/model.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
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
|
-
|
155
|
-
|
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
|
-
|
169
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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.
|
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-
|
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
|