viximo-cache-money 0.3.0

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