ricordami 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. data/CHANGELOG.md +13 -0
  2. data/ISSUES.md +0 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +454 -0
  5. data/TODO.md +21 -0
  6. data/examples/calls.rb +64 -0
  7. data/examples/singers.rb +42 -0
  8. data/lib/ricordami/attribute.rb +52 -0
  9. data/lib/ricordami/can_be_queried.rb +133 -0
  10. data/lib/ricordami/can_be_validated.rb +27 -0
  11. data/lib/ricordami/can_have_relationships.rb +152 -0
  12. data/lib/ricordami/configuration.rb +39 -0
  13. data/lib/ricordami/connection.rb +22 -0
  14. data/lib/ricordami/exceptions.rb +14 -0
  15. data/lib/ricordami/has_attributes.rb +159 -0
  16. data/lib/ricordami/has_indices.rb +105 -0
  17. data/lib/ricordami/is_lockable.rb +52 -0
  18. data/lib/ricordami/is_persisted.rb +123 -0
  19. data/lib/ricordami/is_retrievable.rb +35 -0
  20. data/lib/ricordami/key_namer.rb +63 -0
  21. data/lib/ricordami/model.rb +29 -0
  22. data/lib/ricordami/query.rb +68 -0
  23. data/lib/ricordami/relationship.rb +40 -0
  24. data/lib/ricordami/unique_index.rb +59 -0
  25. data/lib/ricordami/unique_validator.rb +21 -0
  26. data/lib/ricordami/value_index.rb +26 -0
  27. data/lib/ricordami/version.rb +3 -0
  28. data/lib/ricordami.rb +26 -0
  29. data/spec/acceptance/manage_relationships_spec.rb +42 -0
  30. data/spec/acceptance/model_with_validation_spec.rb +78 -0
  31. data/spec/acceptance/query_model_spec.rb +93 -0
  32. data/spec/acceptance_helper.rb +2 -0
  33. data/spec/ricordami/attribute_spec.rb +113 -0
  34. data/spec/ricordami/can_be_queried_spec.rb +254 -0
  35. data/spec/ricordami/can_be_validated_spec.rb +115 -0
  36. data/spec/ricordami/can_have_relationships_spec.rb +255 -0
  37. data/spec/ricordami/configuration_spec.rb +45 -0
  38. data/spec/ricordami/connection_spec.rb +25 -0
  39. data/spec/ricordami/exceptions_spec.rb +43 -0
  40. data/spec/ricordami/has_attributes_spec.rb +266 -0
  41. data/spec/ricordami/has_indices_spec.rb +73 -0
  42. data/spec/ricordami/is_lockable_spec.rb +45 -0
  43. data/spec/ricordami/is_persisted_spec.rb +186 -0
  44. data/spec/ricordami/is_retrievable_spec.rb +55 -0
  45. data/spec/ricordami/key_namer_spec.rb +56 -0
  46. data/spec/ricordami/model_spec.rb +65 -0
  47. data/spec/ricordami/query_spec.rb +156 -0
  48. data/spec/ricordami/relationship_spec.rb +123 -0
  49. data/spec/ricordami/unique_index_spec.rb +87 -0
  50. data/spec/ricordami/unique_validator_spec.rb +41 -0
  51. data/spec/ricordami/value_index_spec.rb +40 -0
  52. data/spec/spec_helper.rb +29 -0
  53. data/spec/support/constants.rb +43 -0
  54. data/spec/support/db_manager.rb +18 -0
  55. data/test/bin/data_loader.rb +107 -0
  56. data/test/data/domains.txt +462 -0
  57. data/test/data/first_names.txt +1220 -0
  58. data/test/data/last_names.txt +1028 -0
  59. data/test/data/people_100_000.csv.bz2 +0 -0
  60. data/test/data/people_10_000.csv.bz2 +0 -0
  61. data/test/data/people_1_000_000.csv.bz2 +0 -0
  62. metadata +258 -0
@@ -0,0 +1,133 @@
1
+ require "ricordami/key_namer"
2
+ require "ricordami/query.rb"
3
+
4
+ module Ricordami
5
+ module CanBeQueried
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ [:where, :and, :not, :any].each do |op|
10
+ define_method(op) do |*args|
11
+ options = args.first || {}
12
+ Query.new(self).send(op, options)
13
+ end
14
+ end
15
+
16
+ def sort(*args)
17
+ Query.new(self).send(:sort, *args)
18
+ end
19
+
20
+ def all(opts = {})
21
+ result_key = run_expressions(opts.delete(:expressions) || [])
22
+ get_result_ids(result_key, opts).map do |id|
23
+ self[id]
24
+ end
25
+ end
26
+
27
+ def paginate(opts = {})
28
+ result_key = run_expressions(opts.delete(:expressions) || [])
29
+ page = opts[:page] || 1
30
+ per_page = opts[:per_page] || 20
31
+ start = (page - 1) * per_page
32
+ opts[:limit] = [start, per_page]
33
+ get_result_ids(result_key, opts).map do |id|
34
+ self[id]
35
+ end
36
+ end
37
+
38
+ def first(opts = {})
39
+ result_key = run_expressions(opts.delete(:expressions) || [])
40
+ opts[:limit] = [0, 1]
41
+ ids = get_result_ids(result_key, opts)
42
+ self[ids.first]
43
+ end
44
+
45
+ def last(opts = {})
46
+ result_key = run_expressions(opts.delete(:expressions) || [])
47
+ size = redis.scard(result_key)
48
+ opts[:limit] = [size - 1, 1]
49
+ ids = get_result_ids(result_key, opts)
50
+ self[ids.first]
51
+ end
52
+
53
+ def rand(opts = {})
54
+ result_key = run_expressions(opts.delete(:expressions) || [])
55
+ size = redis.scard(result_key)
56
+ opts[:limit] = [Kernel.rand(size), 1]
57
+ ids = get_result_ids(result_key, opts)
58
+ self[ids.first]
59
+ end
60
+
61
+ private
62
+
63
+ def run_expressions(expressions)
64
+ key_all_ids = indices[:id].uidx_key_name
65
+ result_key = expressions.reduce(key_all_ids) do |key, expression|
66
+ type, conditions = expression
67
+ condition_keys = get_keys_for_each_condition(conditions)
68
+ next key if condition_keys.empty?
69
+ target_key = key_name_for_expression(type, conditions, key)
70
+ send("run_#{type}", target_key, key, condition_keys)
71
+ end
72
+ result_key.empty?? [] : result_key
73
+ end
74
+
75
+ def get_keys_for_each_condition(conditions)
76
+ conditions.map do |field, value|
77
+ index = indices[field]
78
+ raise MissingIndex.new("class: #{self}, attribute: #{field.inspect}") if index.nil?
79
+ index.key_name_for_value(value)
80
+ end
81
+ end
82
+
83
+ def key_name_for_expression(type, conditions, previous_key)
84
+ KeyNamer.volatile_set(self, :key => previous_key,
85
+ :info => [type] + conditions.keys)
86
+ end
87
+
88
+ def get_result_ids(key, opts)
89
+ return redis.smembers(key) unless opts[:sort_by] || opts[:limit]
90
+ sort_key = KeyNamer.sort(self, :sort_by => opts[:sort_by])
91
+ sort_options = opts.slice(:order, :limit)
92
+ redis.sort(key, sort_options.merge(:by => sort_key))
93
+ end
94
+
95
+ def run_and(key_name, start_key, keys)
96
+ # we get the intersection of the start key and the condition keys
97
+ redis.sinterstore(key_name, start_key, *keys)
98
+ key_name
99
+ end
100
+
101
+ alias :run_where :run_and
102
+
103
+ def run_any(key_name, start_key, keys)
104
+ tmp_key = KeyNamer.temporary(self)
105
+ keys.each_with_index do |key, i|
106
+ if i == 0
107
+ # if only one condition key, :any condition is same as :and condition
108
+ redis.sinterstore(key_name, start_key, keys.first)
109
+ else
110
+ # we get the intersection of the start key with each condition key
111
+ # and we make a union of all of those
112
+ end
113
+ redis.sinterstore(tmp_key, start_key, key)
114
+ redis.sunionstore(key_name, key_name, tmp_key)
115
+ end
116
+ redis.del(tmp_key)
117
+ key_name
118
+ end
119
+
120
+ def run_not(key_name, start_key, keys)
121
+ keys.each_with_index do |key, i|
122
+ redis.sdiffstore(key_name, i == 0 ? start_key : key_name, key)
123
+ end
124
+ key_name
125
+ end
126
+
127
+ def reverse_order(order)
128
+ return order.sub("DESC", "ASC") if order.index("DESC")
129
+ order.sub("ASC", "DESC")
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,27 @@
1
+ require "active_model/validations"
2
+ require "ricordami/unique_validator"
3
+
4
+ module Ricordami
5
+ module CanBeValidated
6
+ extend ActiveSupport::Concern
7
+ include ActiveModel::Validations
8
+
9
+ module ClassMethods
10
+ def validates_uniqueness_of(*attr_names)
11
+ validates_with UniqueValidator, _merge_attributes(attr_names)
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ def valid?
17
+ raise ModelHasBeenDeleted.new("can't validate a deleted model") if deleted?
18
+ super
19
+ end
20
+
21
+ def save(opts = {})
22
+ return false unless opts[:validate] == false || valid?
23
+ super(opts)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,152 @@
1
+ require "ricordami/relationship"
2
+ require "ricordami/can_be_queried"
3
+
4
+ module Ricordami
5
+ module CanHaveRelationships
6
+ extend ActiveSupport::Concern
7
+
8
+ included do |base|
9
+ base.send(:include, CanBeQueried)
10
+ end
11
+
12
+ module ClassMethods
13
+ def relationships
14
+ @relationships ||= {}
15
+ end
16
+
17
+ Relationship::SUPPORTED_TYPES.each do |type|
18
+ define_method(type) do |*args|
19
+ name = args.first
20
+ options = args[1] || {}
21
+ options.merge!(:other => name, :self => self.to_s.underscore.to_sym)
22
+ relationship = Relationship.new(type, options)
23
+ self.relationships[relationship.name] = relationship
24
+ setup_method = :"setup_#{type}"
25
+ send(setup_method, relationship) if respond_to?(setup_method, true)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def set_block_to_delete_dependents(relationship)
32
+ queue_deleting_operations do |obj, session|
33
+ ref_objs = obj.send(relationship.name)
34
+ ref_objs = [ref_objs] if relationship.type == :references_one
35
+ ref_objs.each { |ref_obj| ref_obj.prepare_delete(session) }
36
+ end
37
+ end
38
+
39
+ def set_block_to_nullify_dependents(relationship)
40
+ queue_deleting_operations do |obj, session|
41
+ ref_objs = obj.send(relationship.name)
42
+ ref_objs = [ref_objs] if relationship.type == :references_one
43
+ ref_objs.each do |ref_obj|
44
+ ref_obj.update_attributes(relationship.referrer_id => nil)
45
+ end
46
+ end
47
+ end
48
+
49
+ def lazy_setup_references_many(relationship)
50
+ klass = relationship.object_class
51
+ referrer_id_sym = relationship.referrer_id.to_sym
52
+ define_method(relationship.name) do
53
+ return Query.new([], klass) unless persisted?
54
+ klass.where(referrer_id_sym => self.id)
55
+ end
56
+ case relationship.dependent
57
+ when :delete then set_block_to_delete_dependents(relationship)
58
+ when :nullify then set_block_to_nullify_dependents(relationship)
59
+ end
60
+ end
61
+
62
+ def define_builders(name, klass, referrer_id_sym)
63
+ # define reference build method
64
+ build_method = :"build_#{name}"
65
+ define_method(build_method) do |*args|
66
+ options = args.first || {}
67
+ klass.new(options.merge(referrer_id_sym => self.id))
68
+ end
69
+ # define reference create method
70
+ define_method(:"create_#{name}") do |*args|
71
+ send(build_method, *args).tap { |obj| obj.save }
72
+ end
73
+ end
74
+
75
+ def lazy_setup_references_one(relationship)
76
+ klass = relationship.object_class
77
+ referrer_id_sym = relationship.referrer_id.to_sym
78
+ define_builders(relationship.name, klass, referrer_id_sym)
79
+ # define reference method reader
80
+ define_method(relationship.name) do
81
+ return nil unless persisted?
82
+ klass.where(referrer_id_sym => self.id).first
83
+ end
84
+ case relationship.dependent
85
+ when :delete then set_block_to_delete_dependents(relationship)
86
+ when :nullify then set_block_to_nullify_dependents(relationship)
87
+ end
88
+ end
89
+
90
+ def setup_referenced_in(relationship)
91
+ attribute(relationship.referrer_id, :indexed => :value)
92
+ overide_referrer_id_reader(relationship)
93
+ end
94
+
95
+ def lazy_setup_referenced_in(relationship)
96
+ klass = relationship.object_class
97
+ name = relationship.name
98
+ define_builders(name, klass, relationship.referrer_id.to_sym)
99
+ referrer_var = :"@#{name}"
100
+ define_method(name) do
101
+ referrer = instance_variable_get(referrer_var)
102
+ return referrer unless referrer.nil?
103
+ referrer_id_val = send(relationship.referrer_id)
104
+ return nil if referrer_id_val.nil?
105
+ klass.get(referrer_id_val).tap do |referrer|
106
+ instance_variable_set(referrer_var, referrer)
107
+ end
108
+ end
109
+ end
110
+
111
+ def overide_referrer_id_reader(relationship)
112
+ referrer_var = :"@#{relationship.name}"
113
+ # overide referrer id to sweep cache
114
+ define_method(:"#{relationship.referrer_id}=") do |value|
115
+ instance_variable_set(referrer_var, nil)
116
+ super(value)
117
+ end
118
+ end
119
+ end
120
+
121
+ module InstanceMethods
122
+ private
123
+
124
+ RE_METHOD = /^(build|create)_(.*)$/
125
+
126
+ def method_missing(meth, *args, &blk)
127
+ match = RE_METHOD.match(meth.to_s)
128
+ meth_root = match.nil?? meth : match[2].to_sym
129
+ if relationship = self.class.relationships[meth_root]
130
+ self.class.send(:"lazy_setup_#{relationship.type}", relationship)
131
+ send(meth, *args, &blk)
132
+ else
133
+ super
134
+ end
135
+ end
136
+
137
+ if Object.respond_to?(:respond_to_missing?)
138
+ def respond_to_missing?(meth, include_private)
139
+ self.class.relationships.has_key?(meth)
140
+ end
141
+ else
142
+ def respond_to?(meth)
143
+ match = RE_METHOD.match(meth.to_s)
144
+ meth_root = match.nil?? meth : match[2].to_sym
145
+ return true if self.class.relationships.has_key?(meth_root)
146
+ super
147
+ end
148
+ public :respond_to?
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,39 @@
1
+ module Ricordami
2
+ module Configuration
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def configure(&block)
7
+ raise ArgumentError.new("block missing") unless block_given?
8
+ yield configuration
9
+ end
10
+
11
+ def configuration
12
+ @configuration ||= Config.new
13
+ end
14
+ end
15
+ end
16
+
17
+ class Config
18
+ ATTRIBUTE_NAMES = [:redis_host, :redis_port, :redis_db, :thread_safe]
19
+
20
+ def initialize
21
+ @options = {}
22
+ end
23
+
24
+ def from_hash(options)
25
+ @options = options.slice(*ATTRIBUTE_NAMES)
26
+ end
27
+
28
+ private
29
+
30
+ def method_missing(meth, *args, &blk)
31
+ return @options[meth] if ATTRIBUTE_NAMES.include?(meth)
32
+ if args.length == 1 && match = /^(.*)=$/.match(meth.to_s)
33
+ name = match[1].to_sym
34
+ return @options[name] = args.first if ATTRIBUTE_NAMES.include?(name)
35
+ end
36
+ raise AttributeNotSupported.new("attribute #{meth.to_s.inspect} is not supported")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ module Ricordami
2
+ module Connection
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def driver
7
+ @driver ||= create_driver
8
+ end
9
+
10
+ private
11
+
12
+ def create_driver
13
+ c = self.configuration
14
+ Redis.new(:host => c.redis_host,
15
+ :port => c.redis_port,
16
+ :db => c.redis_db,
17
+ :thread_safe => c.thread_safe,
18
+ :timeout => 10)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module Ricordami
2
+ Error = Class.new(StandardError)
3
+ NotFound = Class.new(Error)
4
+ AttributeNotSupported = Class.new(Error)
5
+ ReadOnlyAttribute = Class.new(Error)
6
+ InvalidIndexDefinition = Class.new(Error)
7
+ ModelHasBeenDeleted = Class.new(Error)
8
+ TypeNotSupported = Class.new(Error)
9
+ MissingIndex = Class.new(Error)
10
+ LockTimeout = Class.new(Error)
11
+ EventNotSupported = Class.new(Error)
12
+ OptionValueInvalid = Class.new(Error)
13
+ MissingMandatoryArgs = Class.new(Error)
14
+ end
@@ -0,0 +1,159 @@
1
+ require "ricordami/key_namer"
2
+ require "ricordami/attribute"
3
+
4
+ module Ricordami
5
+ module HasAttributes
6
+ extend ActiveSupport::Concern
7
+ include ActiveModel::AttributeMethods
8
+ include ActiveModel::Dirty
9
+
10
+ included do
11
+ attribute_method_suffix('', '=')
12
+ attribute :id, :read_only => true,
13
+ :initial => :sequence,
14
+ :type => :string
15
+ end
16
+
17
+ module ClassMethods
18
+ def attributes
19
+ @attributes ||= {}
20
+ end
21
+
22
+ def attribute(name, options = {})
23
+ instance = Attribute.new(name, options)
24
+ options = OptionsExpander.new(self, options)
25
+ self.attributes[name.to_sym] = instance
26
+ index(instance.indexed => name.to_sym) if instance.indexed?
27
+ instance
28
+ end
29
+
30
+ def attributes_key_name_for(id)
31
+ KeyNamer.attributes(self.to_s, :id => id)
32
+ end
33
+ end
34
+
35
+ module InstanceMethods
36
+ attr_reader :attributes
37
+
38
+ def initialize(attrs = {})
39
+ @attributes = {}.with_indifferent_access
40
+ @reloading = false
41
+ update_mem_attributes(attrs) unless attrs.empty?
42
+ set_default_attribute_values
43
+ end
44
+
45
+ # ActiveModel::AttributeMethods doesn't seem
46
+ # to generate a reader for id that uses
47
+ # #attributes["id"] in Ruby 1.8.7, so hard-coding
48
+ # it now
49
+ def id
50
+ @attributes["id"]
51
+ end
52
+
53
+ # Replace attribute values with the hash attrs
54
+ # Note: attrs keys can be strings or symbols
55
+ def update_mem_attributes(attrs)
56
+ @reloading = true
57
+ update_mem_attributes!(attrs)
58
+ @reloading = false
59
+ end
60
+
61
+ def update_mem_attributes!(attrs)
62
+ valid_keys = self.class.attributes.keys
63
+ attrs.symbolize_keys.slice(*valid_keys).each do |name, value|
64
+ send(:"#{name}=", value)
65
+ end
66
+ true
67
+ end
68
+
69
+ def update_mem_attributes(attrs)
70
+ valid_keys = self.class.attributes.keys
71
+ attrs.symbolize_keys.slice(*valid_keys).each do |name, value|
72
+ write_attribute(name, value)
73
+ end
74
+ true
75
+ end
76
+
77
+ private
78
+
79
+ def write_attribute(name, value)
80
+ converted = value.send(self.class.attributes[name].converter) unless value.nil?
81
+ return converted if @attributes[name] == converted
82
+ if @persisted_attributes && @persisted_attributes[name] == converted
83
+ @changed_attributes.delete(name.to_s)
84
+ else
85
+ attribute_will_change!(name.to_s)
86
+ end
87
+ @attributes[name] = converted
88
+ end
89
+
90
+ def attribute(name)
91
+ @attributes[name]
92
+ end
93
+
94
+ def attribute=(name, value)
95
+ raise ModelHasBeenDeleted.new("can't update attribute #{name}") if deleted?
96
+ assert_can_update!(name) unless @reloading
97
+ write_attribute(name.to_sym, value)
98
+ end
99
+
100
+ def assert_can_update!(name)
101
+ definition = self.class.attributes[name.to_sym]
102
+ if definition.read_only? && @attributes[name].present?
103
+ raise ReadOnlyAttribute.new("can't change #{name}")
104
+ end
105
+ end
106
+
107
+ def set_default_attribute_values
108
+ self.class.attributes.each do |name, attribute|
109
+ unless @attributes.has_key?(name)
110
+ @attributes[name] = if attribute.default_value?
111
+ attribute_will_change!(name.to_s)
112
+ attribute.default_value
113
+ else
114
+ nil
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ def set_initial_attribute_values
121
+ self.class.attributes.each do |name, attribute|
122
+ unless @attributes[name].present?
123
+ @attributes[name] = if attribute.initial_value?
124
+ attribute_will_change!(name.to_s)
125
+ attribute.initial_value
126
+ else
127
+ nil
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def attributes_key_name
134
+ @attributes_key_name ||= self.class.attributes_key_name_for(id)
135
+ end
136
+
137
+ def attributes_synced_with_db!
138
+ @persisted_attributes = @attributes.clone
139
+ @previously_changed = changes
140
+ @changed_attributes.clear if @changed_attributes
141
+ end
142
+ end
143
+
144
+ class OptionsExpander
145
+ def initialize(model, opts = {})
146
+ opts.slice(:initial, :default).each do |name, value|
147
+ next unless value.is_a?(Symbol) && respond_to?(value)
148
+ opts[name] = send(value, model)
149
+ end
150
+ opts
151
+ end
152
+
153
+ def sequence(model)
154
+ key = KeyNamer.sequence(model, :type => "id")
155
+ Proc.new { model.redis.incr(key).to_s }
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,105 @@
1
+ require "ricordami/has_attributes"
2
+ require "ricordami/unique_index"
3
+ require "ricordami/value_index"
4
+
5
+ module Ricordami
6
+ module HasIndices
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def indices
11
+ @indices ||= {}
12
+ end
13
+
14
+ def index(options = {})
15
+ # for now we can only create unique indices
16
+ options.assert_valid_keys(:unique, :get_by, :value)
17
+ fields = options.delete(:unique)
18
+ return unique_index(fields, options) if fields.present?
19
+ field = options.delete(:value)
20
+ return value_index(field) if field.present?
21
+ raise InvalidIndexDefinition.new(self.class)
22
+ end
23
+
24
+ private
25
+
26
+ def add_index(index)
27
+ return false if self.indices.has_key?(index.name)
28
+ self.indices[index.name] = index
29
+ end
30
+
31
+ def value_index(field)
32
+ index = ValueIndex.new(self, field)
33
+ return nil unless add_index(index)
34
+ queue_saving_operations do |obj, session|
35
+ old_v = obj.send("#{field}_was")
36
+ new_v = obj.send(field)
37
+ next if old_v == new_v
38
+ if obj.persisted? && old_v.present?
39
+ indices[index.name].rem(obj.id, old_v)
40
+ end
41
+ indices[index.name].add(obj.id, new_v)
42
+ end
43
+ queue_deleting_operations do |obj, session|
44
+ if value = obj.send("#{field}_was")
45
+ indices[index.name].rem(obj.id, value, true).each do |command|
46
+ session.commands << command
47
+ end
48
+ end
49
+ end
50
+ index
51
+ end
52
+
53
+ def unique_index(fields, options = {})
54
+ create_unique_index(fields, options).tap do |index|
55
+ next if index.nil?
56
+ create_unique_get_method(index) if options[:get_by]
57
+ end
58
+ end
59
+
60
+ def create_unique_index(fields, options)
61
+ index = UniqueIndex.new(self, fields, options)
62
+ return nil unless add_index(index)
63
+ queue_saving_operations do |obj, session|
64
+ old_v = serialize_values(index.fields, obj, :previous => true)
65
+ new_v = serialize_values(index.fields, obj)
66
+ next if old_v == new_v
67
+ if obj.persisted? && old_v.present?
68
+ indices[index.name].rem(obj.id, old_v)
69
+ end
70
+ indices[index.name].add(obj.id, new_v)
71
+ end
72
+ queue_deleting_operations do |obj, session|
73
+ if value = serialize_values(index.fields, obj)
74
+ indices[index.name].rem(obj.id, value, true).each do |command|
75
+ session.commands << command
76
+ end
77
+ end
78
+ end
79
+ index
80
+ end
81
+
82
+ def create_unique_get_method(index)
83
+ meth = :"get_by_#{index.fields.map(&:to_s).join("-")}"
84
+ define_singleton_method(meth) do |*args|
85
+ all = redis.hgetall(index.ref_key_name)
86
+ id = index.id_for_values(*args)
87
+ get(id)
88
+ end
89
+ end
90
+
91
+ def serialize_values(fields, obj, opts = {})
92
+ fields.map do |f|
93
+ attr = opts[:previous] ? "#{f}_was" : f
94
+ obj.send(attr)
95
+ end.join(UniqueIndex::SEPARATOR)
96
+ end
97
+
98
+ def define_singleton_method(*args, &block)
99
+ class << self
100
+ self
101
+ end.send(:define_method, *args, &block)
102
+ end unless method_defined? :define_singleton_method
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,52 @@
1
+ require "ricordami/key_namer"
2
+
3
+ module Ricordami
4
+ module IsLockable
5
+ extend ActiveSupport::Concern
6
+
7
+ module InstanceMethods
8
+ # Pretty much stolen from redis objects
9
+ # http://github.com/nateware/redis-objects/blob/master/lib/redis/lock.rb
10
+ def lock!(options = {}, &block)
11
+ key = KeyNamer.lock(self.class, :id => id)
12
+ start = Time.now
13
+ acquired_lock = false
14
+ expiration = nil
15
+ expires_in = options.fetch(:expiration, 15)
16
+ timeout = options.fetch(:timeout, 1)
17
+
18
+ while (Time.now - start) < timeout
19
+ expiration = generate_expiration(expires_in)
20
+ acquired_lock = redis.setnx(key, expiration)
21
+ break if acquired_lock
22
+
23
+ old_expiration = redis.get(key).to_f
24
+
25
+ if old_expiration < Time.now.to_f
26
+ expiration = generate_expiration(expires_in)
27
+ old_expiration = redis.getset(key, expiration).to_f
28
+
29
+ if old_expiration < Time.now.to_f
30
+ acquired_lock = true
31
+ break
32
+ end
33
+ end
34
+
35
+ sleep 0.1
36
+ end
37
+
38
+ raise(LockTimeout.new(key, timeout)) unless acquired_lock
39
+
40
+ begin
41
+ yield
42
+ ensure
43
+ redis.del(key) if expiration > Time.now.to_f
44
+ end
45
+ end
46
+
47
+ def generate_expiration(expiration)
48
+ (Time.now + expiration.to_f).to_f
49
+ end
50
+ end
51
+ end
52
+ end