ricordami 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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