artemk-cache-money 0.2.13.2

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 (48) hide show
  1. data/LICENSE +201 -0
  2. data/README +210 -0
  3. data/README.markdown +210 -0
  4. data/TODO +17 -0
  5. data/UNSUPPORTED_FEATURES +12 -0
  6. data/config/environment.rb +16 -0
  7. data/config/memcached.yml +6 -0
  8. data/db/schema.rb +18 -0
  9. data/init.rb +1 -0
  10. data/lib/cache_money.rb +86 -0
  11. data/lib/cash/accessor.rb +83 -0
  12. data/lib/cash/buffered.rb +129 -0
  13. data/lib/cash/config.rb +82 -0
  14. data/lib/cash/fake.rb +83 -0
  15. data/lib/cash/finders.rb +38 -0
  16. data/lib/cash/index.rb +214 -0
  17. data/lib/cash/local.rb +76 -0
  18. data/lib/cash/lock.rb +63 -0
  19. data/lib/cash/mock.rb +154 -0
  20. data/lib/cash/query/abstract.rb +197 -0
  21. data/lib/cash/query/calculation.rb +45 -0
  22. data/lib/cash/query/primary_key.rb +50 -0
  23. data/lib/cash/query/select.rb +16 -0
  24. data/lib/cash/request.rb +3 -0
  25. data/lib/cash/transactional.rb +43 -0
  26. data/lib/cash/util/active_record.rb +5 -0
  27. data/lib/cash/util/array.rb +9 -0
  28. data/lib/cash/util/marshal.rb +19 -0
  29. data/lib/cash/write_through.rb +69 -0
  30. data/lib/mem_cached_session_store.rb +50 -0
  31. data/lib/mem_cached_support_store.rb +141 -0
  32. data/lib/memcached_wrapper.rb +261 -0
  33. data/spec/cash/accessor_spec.rb +186 -0
  34. data/spec/cash/active_record_spec.rb +224 -0
  35. data/spec/cash/buffered_spec.rb +9 -0
  36. data/spec/cash/calculations_spec.rb +67 -0
  37. data/spec/cash/finders_spec.rb +408 -0
  38. data/spec/cash/local_buffer_spec.rb +9 -0
  39. data/spec/cash/local_spec.rb +9 -0
  40. data/spec/cash/lock_spec.rb +108 -0
  41. data/spec/cash/marshal_spec.rb +60 -0
  42. data/spec/cash/order_spec.rb +172 -0
  43. data/spec/cash/transactional_spec.rb +578 -0
  44. data/spec/cash/window_spec.rb +195 -0
  45. data/spec/cash/write_through_spec.rb +245 -0
  46. data/spec/memcached_wrapper_test.rb +209 -0
  47. data/spec/spec_helper.rb +68 -0
  48. metadata +168 -0
data/lib/cash/fake.rb ADDED
@@ -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,38 @@
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
+ Query::Select.perform(self, options, scope(:find))
25
+ end
26
+
27
+ # User.find(1), User.find(1, 2, 3), User.find([1, 2, 3]), User.find([])
28
+ def find_from_ids_with_cache(ids, options)
29
+ Query::PrimaryKey.perform(self, ids, options, scope(:find))
30
+ end
31
+
32
+ # User.count(:all), User.count, User.sum(...)
33
+ def calculate_with_cache(operation, column_name, options = {})
34
+ Query::Calculation.perform(self, operation, column_name, options, scope(:find))
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/cash/index.rb ADDED
@@ -0,0 +1,214 @@
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
+ DEFAULT_OPTIONS = { :ttl => 1.day }
8
+
9
+ def initialize(config, active_record, attributes, options = {})
10
+ DEFAULT_OPTIONS[:ttl] = config.ttl || DEFAULT_OPTIONS[:ttl]
11
+ @config, @active_record, @attributes, @options = config, active_record, Array(attributes).collect(&:to_s).sort, DEFAULT_OPTIONS.merge(options)
12
+ end
13
+
14
+ def ==(other)
15
+ case other
16
+ when Index
17
+ attributes == other.attributes
18
+ else
19
+ attributes == Array(other)
20
+ end
21
+ end
22
+ alias_method :eql?, :==
23
+
24
+ module Commands
25
+ def add(object)
26
+ _, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
27
+ add_to_index_with_minimal_network_operations(new_attribute_value_pairs, object)
28
+ end
29
+
30
+ def update(object)
31
+ old_attribute_value_pairs, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
32
+ update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
33
+ end
34
+
35
+ def remove(object)
36
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
37
+ remove_from_index_with_minimal_network_operations(old_attribute_value_pairs, object)
38
+ end
39
+
40
+ def delete(object)
41
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
42
+ key = cache_key(old_attribute_value_pairs)
43
+ expire(key)
44
+ end
45
+ end
46
+ include Commands
47
+
48
+ module Attributes
49
+ def ttl
50
+ @ttl ||= options[:ttl] || config.ttl
51
+ end
52
+
53
+ def order
54
+ @order ||= options[:order] || :asc
55
+ end
56
+
57
+ def limit
58
+ options[:limit]
59
+ end
60
+
61
+ def buffer
62
+ options[:buffer]
63
+ end
64
+
65
+ def window
66
+ limit && limit + buffer
67
+ end
68
+
69
+ def order_column
70
+ options[:order_column] || 'id'
71
+ end
72
+ end
73
+ include Attributes
74
+
75
+ def serialize_object(object)
76
+ primary_key? ? object.shallow_clone : object.id
77
+ end
78
+
79
+ def matches?(query)
80
+ query.calculation? ||
81
+ (query.order == [order_column, order] &&
82
+ (!limit || (query.limit && query.limit + query.offset <= limit)))
83
+ end
84
+
85
+ private
86
+ def old_and_new_attribute_value_pairs(object)
87
+ old_attribute_value_pairs = []
88
+ new_attribute_value_pairs = []
89
+ @attributes.each do |name|
90
+ new_value = object.attributes[name]
91
+ if object.changed.include? name
92
+ original_value = object.send("#{name}_was")
93
+ else
94
+ original_value = new_value
95
+ end
96
+ old_attribute_value_pairs << [name, original_value]
97
+ new_attribute_value_pairs << [name, new_value]
98
+ end
99
+ [old_attribute_value_pairs, new_attribute_value_pairs]
100
+ end
101
+
102
+ def add_to_index_with_minimal_network_operations(attribute_value_pairs, object)
103
+ if primary_key?
104
+ add_object_to_primary_key_cache(attribute_value_pairs, object)
105
+ else
106
+ add_object_to_cache(attribute_value_pairs, object)
107
+ end
108
+ end
109
+
110
+ def primary_key?
111
+ @attributes.size == 1 && @attributes.first == primary_key
112
+ end
113
+
114
+ def add_object_to_primary_key_cache(attribute_value_pairs, object)
115
+ set(cache_key(attribute_value_pairs), [serialize_object(object)], :ttl => ttl)
116
+ end
117
+
118
+ def cache_key(attribute_value_pairs)
119
+ attribute_value_pairs.flatten.join('/')
120
+ end
121
+
122
+ def add_object_to_cache(attribute_value_pairs, object, overwrite = true)
123
+ return if invalid_cache_key?(attribute_value_pairs)
124
+
125
+ key, cache_value, cache_hit = get_key_and_value_at_index(attribute_value_pairs)
126
+ if !cache_hit || overwrite
127
+ object_to_add = serialize_object(object)
128
+ objects = (cache_value + [object_to_add]).sort do |a, b|
129
+ (a <=> b) * (order == :asc ? 1 : -1)
130
+ end.uniq
131
+ objects = truncate_if_necessary(objects)
132
+ set(key, objects, :ttl => ttl)
133
+ incr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
134
+ end
135
+ end
136
+
137
+ def invalid_cache_key?(attribute_value_pairs)
138
+ attribute_value_pairs.collect { |_,value| value }.any? { |x| x.nil? }
139
+ end
140
+
141
+ def get_key_and_value_at_index(attribute_value_pairs)
142
+ key = cache_key(attribute_value_pairs)
143
+ cache_hit = true
144
+ cache_value = get(key) do
145
+ cache_hit = false
146
+ conditions = attribute_value_pairs.to_hash_without_nils
147
+ find_every_without_cache(:conditions => conditions, :limit => window).collect do |object|
148
+ serialize_object(object)
149
+ end
150
+ end
151
+ [key, cache_value, cache_hit]
152
+ end
153
+
154
+ def truncate_if_necessary(objects)
155
+ objects.slice(0, window || objects.size)
156
+ end
157
+
158
+ def calculate_at_index(operation, attribute_value_pairs)
159
+ conditions = attribute_value_pairs.to_hash_without_nils
160
+ calculate_without_cache(operation, :all, :conditions => conditions)
161
+ end
162
+
163
+ def update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
164
+ if index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
165
+ remove_object_from_cache(old_attribute_value_pairs, object)
166
+ add_object_to_cache(new_attribute_value_pairs, object)
167
+ elsif primary_key?
168
+ add_object_to_primary_key_cache(new_attribute_value_pairs, object)
169
+ else
170
+ add_object_to_cache(new_attribute_value_pairs, object, false)
171
+ end
172
+ end
173
+
174
+ def index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
175
+ old_attribute_value_pairs != new_attribute_value_pairs
176
+ end
177
+
178
+ def remove_from_index_with_minimal_network_operations(attribute_value_pairs, object)
179
+ if primary_key?
180
+ remove_object_from_primary_key_cache(attribute_value_pairs, object)
181
+ else
182
+ remove_object_from_cache(attribute_value_pairs, object)
183
+ end
184
+ end
185
+
186
+ def remove_object_from_primary_key_cache(attribute_value_pairs, object)
187
+ set(cache_key(attribute_value_pairs), [], :ttl => ttl)
188
+ end
189
+
190
+ def remove_object_from_cache(attribute_value_pairs, object)
191
+ return if invalid_cache_key?(attribute_value_pairs)
192
+
193
+ key, cache_value, _ = get_key_and_value_at_index(attribute_value_pairs)
194
+ object_to_remove = serialize_object(object)
195
+ objects = cache_value - [object_to_remove]
196
+ objects = resize_if_necessary(attribute_value_pairs, objects)
197
+ set(key, objects, :ttl => ttl)
198
+ end
199
+
200
+ def resize_if_necessary(attribute_value_pairs, objects)
201
+ conditions = attribute_value_pairs.to_hash_without_nils
202
+ key = cache_key(attribute_value_pairs)
203
+ count = decr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
204
+
205
+ if limit && objects.size < limit && objects.size < count
206
+ find_every_without_cache(:select => :id, :conditions => conditions).collect do |object|
207
+ serialize_object(object)
208
+ end
209
+ else
210
+ objects
211
+ end
212
+ end
213
+ end
214
+ end
data/lib/cash/local.rb ADDED
@@ -0,0 +1,76 @@
1
+ module Cash
2
+ class Local
3
+ delegate :respond_to?, :to => :@remote_cache
4
+
5
+ def initialize(remote_cache)
6
+ @remote_cache = remote_cache
7
+ end
8
+
9
+ def cache_locally
10
+ @remote_cache = LocalBuffer.new(original_cache = @remote_cache)
11
+ yield if block_given?
12
+ ensure
13
+ @remote_cache = original_cache
14
+ end
15
+
16
+ def autoload_missing_constants
17
+ yield if block_given?
18
+ rescue ArgumentError, MemCache::MemCacheError => error
19
+ lazy_load ||= Hash.new { |hash, hash_key| hash[hash_key] = true; false }
20
+ if error.to_s[/undefined class|referred/] && !lazy_load[error.to_s.split.last.constantize]
21
+ retry
22
+ else
23
+ raise error
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def method_missing(method, *args, &block)
30
+ autoload_missing_constants do
31
+ @remote_cache.send(method, *args, &block)
32
+ end
33
+ end
34
+ end
35
+
36
+ class LocalBuffer
37
+ delegate :respond_to?, :to => :@remote_cache
38
+
39
+ def initialize(remote_cache)
40
+ @local_cache = {}
41
+ @remote_cache = remote_cache
42
+ end
43
+
44
+ def get(key, *options)
45
+ if @local_cache.has_key?(key)
46
+ @local_cache[key]
47
+ else
48
+ @local_cache[key] = @remote_cache.get(key, *options)
49
+ end
50
+ end
51
+
52
+ def set(key, value, *options)
53
+ @remote_cache.set(key, value, *options)
54
+ @local_cache[key] = value
55
+ end
56
+
57
+ def add(key, value, *options)
58
+ result = @remote_cache.add(key, value, *options)
59
+ if result == "STORED\r\n"
60
+ @local_cache[key] = value
61
+ end
62
+ result
63
+ end
64
+
65
+ def delete(key, *options)
66
+ @remote_cache.delete(key, *options)
67
+ @local_cache.delete(key)
68
+ end
69
+
70
+ private
71
+
72
+ def method_missing(method, *args, &block)
73
+ @remote_cache.send(method, *args, &block)
74
+ end
75
+ end
76
+ end
data/lib/cash/lock.rb ADDED
@@ -0,0 +1,63 @@
1
+ require 'socket'
2
+
3
+ module Cash
4
+ class Lock
5
+ class Error < RuntimeError; end
6
+
7
+ INITIAL_WAIT = 2
8
+ DEFAULT_RETRY = 8
9
+ DEFAULT_EXPIRY = 30
10
+
11
+ def initialize(cache)
12
+ @cache = cache
13
+ end
14
+
15
+ def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY, initial_wait = INITIAL_WAIT)
16
+ if recursive_lock?(key)
17
+ yield
18
+ else
19
+ acquire_lock(key, lock_expiry, retries, initial_wait)
20
+ begin
21
+ yield
22
+ ensure
23
+ release_lock(key)
24
+ end
25
+ end
26
+ end
27
+
28
+ def acquire_lock(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY, initial_wait = INITIAL_WAIT)
29
+ retries.times do |count|
30
+ response = @cache.add("lock/#{key}", host_pid, lock_expiry)
31
+ return if response == "STORED\r\n"
32
+ return if recursive_lock?(key)
33
+ exponential_sleep(count, initial_wait) unless count == retries - 1
34
+ end
35
+ debug_lock(key)
36
+ raise Error, "Couldn't acquire memcache lock on #{@cache.get_server_for_key("lock/#{key}")}"
37
+ end
38
+
39
+ def release_lock(key)
40
+ @cache.delete("lock/#{key}")
41
+ end
42
+
43
+ def exponential_sleep(count, initial_wait)
44
+ sleep((2**count) / initial_wait)
45
+ end
46
+
47
+ private
48
+
49
+ def recursive_lock?(key)
50
+ @cache.get("lock/#{key}") == host_pid
51
+ end
52
+
53
+ def debug_lock(key)
54
+ @cache.logger.warn("Cash::Lock[#{key}]: #{@cache.get("lock/#{key}")}") if @cache.respond_to?(:logger) && @cache.logger.respond_to?(:warn)
55
+ rescue
56
+ @cache.logger.warn("#{$!}") if @cache.respond_to?(:logger) && @cache.logger.respond_to?(:warn)
57
+ end
58
+
59
+ def host_pid
60
+ "#{Socket.gethostname} #{Process.pid}"
61
+ end
62
+ end
63
+ end