viximo-cache-money 0.3.0

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 (51) hide show
  1. data/LICENSE +201 -0
  2. data/README +204 -0
  3. data/README.markdown +204 -0
  4. data/TODO +17 -0
  5. data/UNSUPPORTED_FEATURES +13 -0
  6. data/config/environment.rb +8 -0
  7. data/config/memcached.yml +4 -0
  8. data/db/schema.rb +18 -0
  9. data/init.rb +1 -0
  10. data/lib/cache_money.rb +105 -0
  11. data/lib/cash/accessor.rb +83 -0
  12. data/lib/cash/adapter/memcache_client.rb +36 -0
  13. data/lib/cash/adapter/memcached.rb +127 -0
  14. data/lib/cash/adapter/redis.rb +144 -0
  15. data/lib/cash/buffered.rb +137 -0
  16. data/lib/cash/config.rb +78 -0
  17. data/lib/cash/fake.rb +83 -0
  18. data/lib/cash/finders.rb +50 -0
  19. data/lib/cash/index.rb +211 -0
  20. data/lib/cash/local.rb +105 -0
  21. data/lib/cash/lock.rb +63 -0
  22. data/lib/cash/mock.rb +158 -0
  23. data/lib/cash/query/abstract.rb +219 -0
  24. data/lib/cash/query/calculation.rb +45 -0
  25. data/lib/cash/query/primary_key.rb +50 -0
  26. data/lib/cash/query/select.rb +16 -0
  27. data/lib/cash/request.rb +3 -0
  28. data/lib/cash/transactional.rb +43 -0
  29. data/lib/cash/util/array.rb +9 -0
  30. data/lib/cash/util/marshal.rb +19 -0
  31. data/lib/cash/version.rb +3 -0
  32. data/lib/cash/write_through.rb +71 -0
  33. data/lib/mem_cached_session_store.rb +49 -0
  34. data/lib/mem_cached_support_store.rb +143 -0
  35. data/rails/init.rb +1 -0
  36. data/spec/cash/accessor_spec.rb +186 -0
  37. data/spec/cash/active_record_spec.rb +224 -0
  38. data/spec/cash/buffered_spec.rb +9 -0
  39. data/spec/cash/calculations_spec.rb +78 -0
  40. data/spec/cash/finders_spec.rb +455 -0
  41. data/spec/cash/local_buffer_spec.rb +9 -0
  42. data/spec/cash/local_spec.rb +9 -0
  43. data/spec/cash/lock_spec.rb +110 -0
  44. data/spec/cash/marshal_spec.rb +60 -0
  45. data/spec/cash/order_spec.rb +172 -0
  46. data/spec/cash/transactional_spec.rb +602 -0
  47. data/spec/cash/window_spec.rb +195 -0
  48. data/spec/cash/without_caching_spec.rb +32 -0
  49. data/spec/cash/write_through_spec.rb +252 -0
  50. data/spec/spec_helper.rb +87 -0
  51. metadata +300 -0
@@ -0,0 +1,137 @@
1
+ module Cash
2
+ class Buffered
3
+ def self.push(cache, lock)
4
+ if cache.is_a?(Buffered)
5
+ cache.push
6
+ else
7
+ Buffered.new(cache, lock)
8
+ end
9
+ end
10
+
11
+ def initialize(memcache, lock)
12
+ @buffer = {}
13
+ @commands = []
14
+ @cache = memcache
15
+ @lock = lock
16
+ end
17
+
18
+ def pop
19
+ @cache
20
+ end
21
+
22
+ def push
23
+ NestedBuffered.new(self, @lock)
24
+ end
25
+
26
+ def get(key, *options)
27
+ if @buffer.has_key?(key)
28
+ # Since buffered ActiveRecord objects can be modified before they get
29
+ # written to the repository, a shallow clone should always be returned
30
+ if @buffer[key].is_a?(Array) && @buffer[key][0].is_a?(ActiveRecord::Base)
31
+ @buffer[key].map(&:shallow_clone)
32
+ elsif @buffer[key].is_a?(ActiveRecord::Base)
33
+ @buffer[key].shallow_clone
34
+ else
35
+ @buffer[key]
36
+ end
37
+ else
38
+ @buffer[key] = @cache.get(key, *options)
39
+ end
40
+ end
41
+
42
+ def set(key, value, *options)
43
+ @buffer[key] = value
44
+ buffer_command Command.new(:set, key, value, *options)
45
+ end
46
+
47
+ def incr(key, amount = 1)
48
+ return unless value = get(key, true)
49
+
50
+ @buffer[key] = value.to_i + amount
51
+ buffer_command Command.new(:incr, key, amount)
52
+ @buffer[key]
53
+ end
54
+
55
+ def decr(key, amount = 1)
56
+ return unless value = get(key, true)
57
+
58
+ @buffer[key] = [value.to_i - amount, 0].max
59
+ buffer_command Command.new(:decr, key, amount)
60
+ @buffer[key]
61
+ end
62
+
63
+ def add(key, value, *options)
64
+ @buffer[key] = value
65
+ buffer_command Command.new(:add, key, value, *options)
66
+ end
67
+
68
+ def delete(key, *options)
69
+ @buffer[key] = nil
70
+ buffer_command Command.new(:delete, key, *options)
71
+ end
72
+
73
+ def get_multi(*keys)
74
+ values = keys.collect { |key| get(key) }
75
+ keys.zip(values).to_hash_without_nils
76
+ end
77
+
78
+ def flush
79
+ sorted_keys = @commands.select(&:requires_lock?).collect(&:key).uniq.sort
80
+ sorted_keys.each do |key|
81
+ @lock.acquire_lock(key)
82
+ end
83
+ perform_commands
84
+ ensure
85
+ @buffer = {}
86
+ sorted_keys.each do |key|
87
+ @lock.release_lock(key)
88
+ end
89
+ end
90
+
91
+ def respond_to?(method)
92
+ @cache.respond_to?(method)
93
+ end
94
+
95
+ protected
96
+
97
+ def perform_commands
98
+ @commands.each do |command|
99
+ command.call(@cache)
100
+ end
101
+ end
102
+
103
+ def buffer_command(command)
104
+ @commands << command
105
+ end
106
+
107
+ private
108
+
109
+ def method_missing(method, *args, &block)
110
+ @cache.send(method, *args, &block)
111
+ end
112
+ end
113
+
114
+ class NestedBuffered < Buffered
115
+ def flush
116
+ perform_commands
117
+ end
118
+ end
119
+
120
+ class Command
121
+ attr_accessor :key
122
+
123
+ def initialize(name, key, *args)
124
+ @name = name
125
+ @key = key
126
+ @args = args
127
+ end
128
+
129
+ def requires_lock?
130
+ @name == :set
131
+ end
132
+
133
+ def call(cache)
134
+ cache.send @name, @key, *@args
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,78 @@
1
+ module Cash
2
+ module Config
3
+ def self.create(active_record, options, indices = [])
4
+ active_record.cache_config = Cash::Config::Config.new(active_record, options)
5
+ indices.each { |i| active_record.index i.attributes, i.options }
6
+ end
7
+
8
+ def self.included(a_module)
9
+ a_module.module_eval do
10
+ extend ClassMethods
11
+ delegate :repository, :to => "self.class"
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def self.extended(a_class)
17
+ class << a_class
18
+ def cache_config
19
+ @cache_config
20
+ end
21
+
22
+ delegate :repository, :indices, :to => :cache_config
23
+ alias_method_chain :inherited, :cache_config
24
+ end
25
+ end
26
+
27
+ def inherited_with_cache_config(subclass)
28
+ inherited_without_cache_config(subclass)
29
+ @cache_config.inherit(subclass) if @cache_config
30
+ end
31
+
32
+ def index(attributes, options = {})
33
+ options.assert_valid_keys(:ttl, :order, :limit, :buffer, :order_column)
34
+ (@cache_config.indices.unshift(Index.new(@cache_config, self, attributes, options))).uniq!
35
+ end
36
+
37
+ def version(number)
38
+ @cache_config.options[:version] = number
39
+ end
40
+
41
+ def cache_config=(config)
42
+ @cache_config = config
43
+ end
44
+
45
+ def cacheable?(*args)
46
+ Cash.enabled && cache_config
47
+ end
48
+ end
49
+
50
+ class Config
51
+ attr_reader :active_record, :options
52
+
53
+ def initialize(active_record, options = {})
54
+ @active_record, @options = active_record, options
55
+ end
56
+
57
+ def repository
58
+ @options[:repository]
59
+ end
60
+
61
+ def ttl
62
+ @ttl ||= (repository.respond_to?(:default_ttl) && repository.default_ttl) || @options[:ttl]
63
+ end
64
+
65
+ def version
66
+ @options[:version] || 1
67
+ end
68
+
69
+ def indices
70
+ @indices ||= active_record == ActiveRecord::Base ? [] : [Index.new(self, active_record, active_record.primary_key)]
71
+ end
72
+
73
+ def inherit(active_record)
74
+ Cash::Config.create(active_record, @options, indices)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,83 @@
1
+ module Cash
2
+ class Fake < HashWithIndifferentAccess
3
+ attr_accessor :servers
4
+
5
+ def get_multi(*keys)
6
+ slice(*keys).collect { |k,v| [k, Marshal.load(v)] }.to_hash
7
+ end
8
+
9
+ def set(key, value, ttl = 0, raw = false)
10
+ self[key] = marshal(value, raw)
11
+ end
12
+
13
+ def get(key, raw = false)
14
+ if raw
15
+ self[key]
16
+ else
17
+ if self.has_key?(key)
18
+ Marshal.load(self[key])
19
+ else
20
+ nil
21
+ end
22
+ end
23
+ end
24
+
25
+ def incr(key, amount = 1)
26
+ if self.has_key?(key)
27
+ self[key] = (self[key].to_i + amount).to_s
28
+ self[key].to_i
29
+ end
30
+ end
31
+
32
+ def decr(key, amount = 1)
33
+ if self.has_key?(key)
34
+ self[key] = (self[key].to_i - amount).to_s
35
+ self[key].to_i
36
+ end
37
+ end
38
+
39
+ def add(key, value, ttl = 0, raw = false)
40
+ return false if self.has_key?(key)
41
+
42
+ self[key] = marshal(value, raw)
43
+ true
44
+ end
45
+
46
+ def append(key, value)
47
+ set(key, get(key, true).to_s + value.to_s, nil, true)
48
+ end
49
+
50
+ def namespace
51
+ nil
52
+ end
53
+
54
+ def flush_all
55
+ clear
56
+ end
57
+
58
+ def stats
59
+ {}
60
+ end
61
+
62
+ def reset_runtime
63
+ [0, Hash.new(0)]
64
+ end
65
+
66
+ private
67
+ def marshal(value, raw)
68
+ if raw
69
+ value.to_s
70
+ else
71
+ Marshal.dump(value)
72
+ end
73
+ end
74
+
75
+ def unmarshal(marshaled_obj)
76
+ Marshal.load(marshaled_obj)
77
+ end
78
+
79
+ def deep_clone(obj)
80
+ unmarshal(marshal(obj))
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,50 @@
1
+ module Cash
2
+ module Finders
3
+ def self.included(active_record_class)
4
+ active_record_class.class_eval do
5
+ extend ClassMethods
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ def self.extended(active_record_class)
11
+ class << active_record_class
12
+ alias_method_chain :find_every, :cache
13
+ alias_method_chain :find_from_ids, :cache
14
+ alias_method_chain :calculate, :cache
15
+ end
16
+ end
17
+
18
+ def without_cache(&block)
19
+ with_scope(:find => {:readonly => true}, &block)
20
+ end
21
+
22
+ # User.find(:first, ...), User.find_by_foo(...), User.find(:all, ...), User.find_all_by_foo(...)
23
+ def find_every_with_cache(options)
24
+ if cacheable?
25
+ Query::Select.perform(self, options, scope(:find))
26
+ else
27
+ find_every_without_cache(options)
28
+ end
29
+ end
30
+
31
+ # User.find(1), User.find(1, 2, 3), User.find([1, 2, 3]), User.find([])
32
+ def find_from_ids_with_cache(ids, options)
33
+ if cacheable?
34
+ Query::PrimaryKey.perform(self, ids, options, scope(:find))
35
+ else
36
+ find_from_ids_without_cache(ids, options)
37
+ end
38
+ end
39
+
40
+ # User.count(:all), User.count, User.sum(...)
41
+ def calculate_with_cache(operation, column_name, options = {})
42
+ if cacheable?
43
+ Query::Calculation.perform(self, operation, column_name, options, scope(:find))
44
+ else
45
+ calculate_without_cache(operation, column_name, options)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,211 @@
1
+ module Cash
2
+ class Index
3
+ attr_reader :attributes, :options
4
+ delegate :each, :hash, :to => :@attributes
5
+ delegate :get, :set, :expire, :find_every_without_cache, :calculate_without_cache, :calculate_with_cache, :incr, :decr, :primary_key, :logger, :to => :@active_record
6
+
7
+ def initialize(config, active_record, attributes, options = {})
8
+ @config, @active_record, @attributes, @options = config, active_record, Array(attributes).collect(&:to_s).sort, options
9
+ end
10
+
11
+ def ==(other)
12
+ case other
13
+ when Index
14
+ attributes == other.attributes
15
+ else
16
+ attributes == Array(other)
17
+ end
18
+ end
19
+ alias_method :eql?, :==
20
+
21
+ module Commands
22
+ def add(object)
23
+ _, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
24
+ add_to_index_with_minimal_network_operations(new_attribute_value_pairs, object)
25
+ end
26
+
27
+ def update(object)
28
+ old_attribute_value_pairs, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
29
+ update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
30
+ end
31
+
32
+ def remove(object)
33
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
34
+ remove_from_index_with_minimal_network_operations(old_attribute_value_pairs, object)
35
+ end
36
+
37
+ def delete(object)
38
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
39
+ key = cache_key(old_attribute_value_pairs)
40
+ expire(key)
41
+ end
42
+ end
43
+ include Commands
44
+
45
+ module Attributes
46
+ def ttl
47
+ @ttl ||= options[:ttl] || @config.ttl
48
+ end
49
+
50
+ def order
51
+ @order ||= options[:order] || :asc
52
+ end
53
+
54
+ def limit
55
+ options[:limit]
56
+ end
57
+
58
+ def buffer
59
+ options[:buffer]
60
+ end
61
+
62
+ def window
63
+ limit && limit + buffer
64
+ end
65
+
66
+ def order_column
67
+ options[:order_column] || 'id'
68
+ end
69
+ end
70
+ include Attributes
71
+
72
+ def serialize_object(object)
73
+ primary_key? ? object.shallow_clone : object.id
74
+ end
75
+
76
+ def matches?(query)
77
+ query.calculation? ||
78
+ (query.order == [order_column, order] &&
79
+ (!limit || (query.limit && query.limit + query.offset <= limit)))
80
+ end
81
+
82
+ private
83
+ def old_and_new_attribute_value_pairs(object)
84
+ old_attribute_value_pairs = []
85
+ new_attribute_value_pairs = []
86
+ @attributes.each do |name|
87
+ new_value = object.attributes[name]
88
+ if object.changed.include? name
89
+ original_value = object.send("#{name}_was")
90
+ else
91
+ original_value = new_value
92
+ end
93
+ old_attribute_value_pairs << [name, original_value]
94
+ new_attribute_value_pairs << [name, new_value]
95
+ end
96
+ [old_attribute_value_pairs, new_attribute_value_pairs]
97
+ end
98
+
99
+ def add_to_index_with_minimal_network_operations(attribute_value_pairs, object)
100
+ if primary_key?
101
+ add_object_to_primary_key_cache(attribute_value_pairs, object)
102
+ else
103
+ add_object_to_cache(attribute_value_pairs, object)
104
+ end
105
+ end
106
+
107
+ def primary_key?
108
+ @attributes.size == 1 && @attributes.first == primary_key
109
+ end
110
+
111
+ def add_object_to_primary_key_cache(attribute_value_pairs, object)
112
+ set(cache_key(attribute_value_pairs), [serialize_object(object)], :ttl => ttl)
113
+ end
114
+
115
+ def cache_key(attribute_value_pairs)
116
+ attribute_value_pairs.flatten.join('/')
117
+ end
118
+
119
+ def add_object_to_cache(attribute_value_pairs, object, overwrite = true)
120
+ return if invalid_cache_key?(attribute_value_pairs)
121
+
122
+ key, cache_value, cache_hit = get_key_and_value_at_index(attribute_value_pairs)
123
+ if !cache_hit || overwrite
124
+ object_to_add = serialize_object(object)
125
+ objects = (cache_value + [object_to_add]).sort do |a, b|
126
+ (a <=> b) * (order == :asc ? 1 : -1)
127
+ end.uniq
128
+ objects = truncate_if_necessary(objects)
129
+ set(key, objects, :ttl => ttl)
130
+ incr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
131
+ end
132
+ end
133
+
134
+ def invalid_cache_key?(attribute_value_pairs)
135
+ attribute_value_pairs.collect { |_,value| value }.any? { |x| x.nil? }
136
+ end
137
+
138
+ def get_key_and_value_at_index(attribute_value_pairs)
139
+ key = cache_key(attribute_value_pairs)
140
+ cache_hit = true
141
+ cache_value = get(key) do
142
+ cache_hit = false
143
+ conditions = attribute_value_pairs.to_hash_without_nils
144
+ find_every_without_cache(:conditions => conditions, :limit => window).collect do |object|
145
+ serialize_object(object)
146
+ end
147
+ end
148
+ [key, cache_value, cache_hit]
149
+ end
150
+
151
+ def truncate_if_necessary(objects)
152
+ objects.slice(0, window || objects.size)
153
+ end
154
+
155
+ def calculate_at_index(operation, attribute_value_pairs)
156
+ conditions = attribute_value_pairs.to_hash_without_nils
157
+ calculate_without_cache(operation, :all, :conditions => conditions)
158
+ end
159
+
160
+ def update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
161
+ if index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
162
+ remove_object_from_cache(old_attribute_value_pairs, object)
163
+ add_object_to_cache(new_attribute_value_pairs, object)
164
+ elsif primary_key?
165
+ add_object_to_primary_key_cache(new_attribute_value_pairs, object)
166
+ else
167
+ add_object_to_cache(new_attribute_value_pairs, object, false)
168
+ end
169
+ end
170
+
171
+ def index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
172
+ old_attribute_value_pairs != new_attribute_value_pairs
173
+ end
174
+
175
+ def remove_from_index_with_minimal_network_operations(attribute_value_pairs, object)
176
+ if primary_key?
177
+ remove_object_from_primary_key_cache(attribute_value_pairs, object)
178
+ else
179
+ remove_object_from_cache(attribute_value_pairs, object)
180
+ end
181
+ end
182
+
183
+ def remove_object_from_primary_key_cache(attribute_value_pairs, object)
184
+ set(cache_key(attribute_value_pairs), [], :ttl => ttl)
185
+ end
186
+
187
+ def remove_object_from_cache(attribute_value_pairs, object)
188
+ return if invalid_cache_key?(attribute_value_pairs)
189
+
190
+ key, cache_value, _ = get_key_and_value_at_index(attribute_value_pairs)
191
+ object_to_remove = serialize_object(object)
192
+ objects = cache_value - [object_to_remove]
193
+ objects = resize_if_necessary(attribute_value_pairs, objects)
194
+ set(key, objects, :ttl => ttl)
195
+ end
196
+
197
+ def resize_if_necessary(attribute_value_pairs, objects)
198
+ conditions = attribute_value_pairs.to_hash_without_nils
199
+ key = cache_key(attribute_value_pairs)
200
+ count = decr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
201
+
202
+ if limit && objects.size < limit && objects.size < count
203
+ find_every_without_cache(:select => :id, :conditions => conditions).collect do |object|
204
+ serialize_object(object)
205
+ end
206
+ else
207
+ objects
208
+ end
209
+ end
210
+ end
211
+ end