sreeix-cache-money 0.2.24.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 (49) 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 +13 -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 +91 -0
  11. data/lib/cash/accessor.rb +83 -0
  12. data/lib/cash/buffered.rb +129 -0
  13. data/lib/cash/config.rb +75 -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 +210 -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/array.rb +9 -0
  27. data/lib/cash/util/marshal.rb +19 -0
  28. data/lib/cash/write_through.rb +69 -0
  29. data/lib/mem_cached_session_store.rb +49 -0
  30. data/lib/mem_cached_support_store.rb +135 -0
  31. data/lib/memcached_wrapper.rb +263 -0
  32. data/rails/init.rb +38 -0
  33. data/spec/cash/accessor_spec.rb +159 -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 +78 -0
  37. data/spec/cash/finders_spec.rb +430 -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 +138 -0
  43. data/spec/cash/shared.rb +62 -0
  44. data/spec/cash/transactional_spec.rb +578 -0
  45. data/spec/cash/window_spec.rb +195 -0
  46. data/spec/cash/write_through_spec.rb +245 -0
  47. data/spec/memcached_wrapper_test.rb +209 -0
  48. data/spec/spec_helper.rb +60 -0
  49. metadata +151 -0
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.to_i }
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
data/lib/cash/mock.rb ADDED
@@ -0,0 +1,154 @@
1
+ module Cash
2
+ class Mock < HashWithIndifferentAccess
3
+ attr_accessor :servers
4
+
5
+ class CacheEntry
6
+ attr_reader :value
7
+
8
+ def self.default_ttl
9
+ 1_000_000
10
+ end
11
+
12
+ def self.now
13
+ Time.now
14
+ end
15
+
16
+ def initialize(value, raw, ttl)
17
+ if raw
18
+ @value = value.to_s
19
+ else
20
+ @value = Marshal.dump(value)
21
+ end
22
+
23
+ if ttl.zero?
24
+ @ttl = self.class.default_ttl
25
+ else
26
+ @ttl = ttl
27
+ end
28
+
29
+ @expires_at = self.class.now + @ttl
30
+ end
31
+
32
+
33
+ def expired?
34
+ self.class.now > @expires_at
35
+ end
36
+
37
+ def increment(amount = 1)
38
+ @value = (@value.to_i + amount).to_s
39
+ end
40
+
41
+ def decrement(amount = 1)
42
+ @value = (@value.to_i - amount).to_s
43
+ end
44
+
45
+ def unmarshal
46
+ Marshal.load(@value)
47
+ end
48
+
49
+ def to_i
50
+ @value.to_i
51
+ end
52
+ end
53
+
54
+ attr_accessor :logging
55
+
56
+ def initialize
57
+ @logging = false
58
+ end
59
+
60
+ def get_multi(keys)
61
+ slice(*keys).collect { |k,v| [k, v.unmarshal] }.to_hash_without_nils
62
+ end
63
+
64
+ def set(key, value, ttl = CacheEntry.default_ttl, raw = false)
65
+ log "< set #{key} #{ttl}"
66
+ self[key] = CacheEntry.new(value, raw, ttl)
67
+ log('> STORED')
68
+ end
69
+
70
+ def get(key, raw = false)
71
+ log "< get #{key}"
72
+ unless self.has_unexpired_key?(key)
73
+ log('> END')
74
+ return nil
75
+ end
76
+
77
+ log("> sending key #{key}")
78
+ log('> END')
79
+ if raw
80
+ self[key].value
81
+ else
82
+ self[key].unmarshal
83
+ end
84
+ end
85
+
86
+ def delete(key, options = {})
87
+ log "< delete #{key}"
88
+ if self.has_unexpired_key?(key)
89
+ log "> DELETED"
90
+ super(key)
91
+ else
92
+ log "> NOT FOUND"
93
+ end
94
+ end
95
+
96
+ def incr(key, amount = 1)
97
+ if self.has_unexpired_key?(key)
98
+ self[key].increment(amount)
99
+ self[key].to_i
100
+ end
101
+ end
102
+
103
+ def decr(key, amount = 1)
104
+ if self.has_unexpired_key?(key)
105
+ self[key].decrement(amount)
106
+ self[key].to_i
107
+ end
108
+ end
109
+
110
+ def add(key, value, ttl = CacheEntry.default_ttl, raw = false)
111
+ if self.has_unexpired_key?(key)
112
+ "NOT_STORED\r\n"
113
+ else
114
+ set(key, value, ttl, raw)
115
+ "STORED\r\n"
116
+ end
117
+ end
118
+
119
+ def append(key, value)
120
+ set(key, get(key, true).to_s + value.to_s, nil, true)
121
+ end
122
+
123
+ def namespace
124
+ nil
125
+ end
126
+
127
+ def flush_all
128
+ log('< flush_all')
129
+ clear
130
+ end
131
+
132
+ def stats
133
+ {}
134
+ end
135
+
136
+ def reset_runtime
137
+ [0, Hash.new(0)]
138
+ end
139
+
140
+ def has_unexpired_key?(key)
141
+ self.has_key?(key) && !self[key].expired?
142
+ end
143
+
144
+ def log(message)
145
+ return unless logging
146
+ logger.debug(message)
147
+ end
148
+
149
+ def logger
150
+ @logger ||= ActiveSupport::BufferedLogger.new(Rails.root.join('log/cash_mock.log'))
151
+ end
152
+
153
+ end
154
+ end