ashleym1972-cache-money 0.2.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cash/index.rb ADDED
@@ -0,0 +1,207 @@
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, :to => :@active_record
6
+
7
+ DEFAULT_OPTIONS = { :ttl => 1.day }
8
+
9
+ def initialize(config, active_record, attributes, options = {})
10
+ @config, @active_record, @attributes, @options = config, active_record, Array(attributes).collect(&:to_s).sort, DEFAULT_OPTIONS.merge(options)
11
+ end
12
+
13
+ def ==(other)
14
+ case other
15
+ when Index
16
+ attributes == other.attributes
17
+ else
18
+ attributes == Array(other)
19
+ end
20
+ end
21
+ alias_method :eql?, :==
22
+
23
+ module Commands
24
+ def add(object)
25
+ clone = object.shallow_clone
26
+ _, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
27
+ add_to_index_with_minimal_network_operations(new_attribute_value_pairs, clone)
28
+ end
29
+
30
+ def update(object)
31
+ clone = object.shallow_clone
32
+ old_attribute_value_pairs, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
33
+ update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, clone)
34
+ end
35
+
36
+ def remove(object)
37
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
38
+ remove_from_index_with_minimal_network_operations(old_attribute_value_pairs, object)
39
+ end
40
+
41
+ def delete(object)
42
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
43
+ key = cache_key(old_attribute_value_pairs)
44
+ expire(key)
45
+ end
46
+ end
47
+ include Commands
48
+
49
+ module Attributes
50
+ def ttl
51
+ @ttl ||= options[:ttl] || config.ttl
52
+ end
53
+
54
+ def order
55
+ @order ||= options[:order] || :asc
56
+ end
57
+
58
+ def limit
59
+ options[:limit]
60
+ end
61
+
62
+ def buffer
63
+ options[:buffer]
64
+ end
65
+
66
+ def window
67
+ limit && limit + buffer
68
+ end
69
+ end
70
+ include Attributes
71
+
72
+ def serialize_object(object)
73
+ primary_key? ? object : object.id
74
+ end
75
+
76
+ def matches?(query)
77
+ query.calculation? ||
78
+ (query.order == ['id', 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
+ original_value = object.send("#{name}_was")
89
+ old_attribute_value_pairs << [name, original_value]
90
+ new_attribute_value_pairs << [name, new_value]
91
+ end
92
+ [old_attribute_value_pairs, new_attribute_value_pairs]
93
+ end
94
+
95
+ def add_to_index_with_minimal_network_operations(attribute_value_pairs, object)
96
+ if primary_key?
97
+ add_object_to_primary_key_cache(attribute_value_pairs, object)
98
+ else
99
+ add_object_to_cache(attribute_value_pairs, object)
100
+ end
101
+ end
102
+
103
+ def primary_key?
104
+ @attributes.size == 1 && @attributes.first == primary_key
105
+ end
106
+
107
+ def add_object_to_primary_key_cache(attribute_value_pairs, object)
108
+ set(cache_key(attribute_value_pairs), [object], :ttl => ttl)
109
+ end
110
+
111
+ def cache_key(attribute_value_pairs)
112
+ attribute_value_pairs.flatten.join('/')
113
+ end
114
+
115
+ def add_object_to_cache(attribute_value_pairs, object, overwrite = true)
116
+ return if invalid_cache_key?(attribute_value_pairs)
117
+
118
+ key, cache_value, cache_hit = get_key_and_value_at_index(attribute_value_pairs)
119
+ if !cache_hit || overwrite
120
+ object_to_add = serialize_object(object)
121
+ objects = (cache_value + [object_to_add]).sort do |a, b|
122
+ (a <=> b) * (order == :asc ? 1 : -1)
123
+ end.uniq
124
+ objects = truncate_if_necessary(objects)
125
+ set(key, objects, :ttl => ttl)
126
+ incr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
127
+ end
128
+ end
129
+
130
+ def invalid_cache_key?(attribute_value_pairs)
131
+ attribute_value_pairs.collect { |_,value| value }.any? { |x| x.nil? }
132
+ end
133
+
134
+ def get_key_and_value_at_index(attribute_value_pairs)
135
+ key = cache_key(attribute_value_pairs)
136
+ cache_hit = true
137
+ cache_value = get(key) do
138
+ cache_hit = false
139
+ conditions = attribute_value_pairs.to_hash_without_nils
140
+ find_every_without_cache(:conditions => conditions, :limit => window).collect do |object|
141
+ serialize_object(object)
142
+ end
143
+ end
144
+ [key, cache_value, cache_hit]
145
+ end
146
+
147
+ def truncate_if_necessary(objects)
148
+ objects.slice(0, window || objects.size)
149
+ end
150
+
151
+ def calculate_at_index(operation, attribute_value_pairs)
152
+ conditions = attribute_value_pairs.to_hash_without_nils
153
+ calculate_without_cache(operation, :all, :conditions => conditions)
154
+ end
155
+
156
+ def update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
157
+ if index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
158
+ remove_object_from_cache(old_attribute_value_pairs, object)
159
+ add_object_to_cache(new_attribute_value_pairs, object)
160
+ elsif primary_key?
161
+ add_object_to_primary_key_cache(new_attribute_value_pairs, object)
162
+ else
163
+ add_object_to_cache(new_attribute_value_pairs, object, false)
164
+ end
165
+ end
166
+
167
+ def index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
168
+ old_attribute_value_pairs != new_attribute_value_pairs
169
+ end
170
+
171
+ def remove_from_index_with_minimal_network_operations(attribute_value_pairs, object)
172
+ if primary_key?
173
+ remove_object_from_primary_key_cache(attribute_value_pairs, object)
174
+ else
175
+ remove_object_from_cache(attribute_value_pairs, object)
176
+ end
177
+ end
178
+
179
+ def remove_object_from_primary_key_cache(attribute_value_pairs, object)
180
+ set(cache_key(attribute_value_pairs), [], :ttl => ttl)
181
+ end
182
+
183
+ def remove_object_from_cache(attribute_value_pairs, object)
184
+ return if invalid_cache_key?(attribute_value_pairs)
185
+
186
+ key, cache_value, _ = get_key_and_value_at_index(attribute_value_pairs)
187
+ object_to_remove = serialize_object(object)
188
+ objects = cache_value - [object_to_remove]
189
+ objects = resize_if_necessary(attribute_value_pairs, objects)
190
+ set(key, objects, :ttl => ttl)
191
+ end
192
+
193
+ def resize_if_necessary(attribute_value_pairs, objects)
194
+ conditions = attribute_value_pairs.to_hash_without_nils
195
+ key = cache_key(attribute_value_pairs)
196
+ count = decr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
197
+
198
+ if limit && objects.size < limit && objects.size < count
199
+ find_every_without_cache(:select => :id, :conditions => conditions).collect do |object|
200
+ serialize_object(object)
201
+ end
202
+ else
203
+ objects
204
+ end
205
+ end
206
+ end
207
+ end
data/lib/cash/local.rb ADDED
@@ -0,0 +1,59 @@
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
12
+ ensure
13
+ @remote_cache = original_cache
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ @remote_cache.send(method, *args, &block)
18
+ end
19
+ end
20
+
21
+ class LocalBuffer
22
+ delegate :respond_to?, :to => :@remote_cache
23
+
24
+ def initialize(remote_cache)
25
+ @local_cache = {}
26
+ @remote_cache = remote_cache
27
+ end
28
+
29
+ def get(key, *options)
30
+ if @local_cache.has_key?(key)
31
+ @local_cache[key]
32
+ else
33
+ @local_cache[key] = @remote_cache.get(key, *options)
34
+ end
35
+ end
36
+
37
+ def set(key, value, *options)
38
+ @remote_cache.set(key, value, *options)
39
+ @local_cache[key] = value
40
+ end
41
+
42
+ def add(key, value, *options)
43
+ result = @remote_cache.add(key, value, *options)
44
+ if result == "STORED\r\n"
45
+ @local_cache[key] = value
46
+ end
47
+ result
48
+ end
49
+
50
+ def delete(key, *options)
51
+ @remote_cache.delete(key, *options)
52
+ @local_cache.delete(key)
53
+ end
54
+
55
+ def method_missing(method, *args, &block)
56
+ @remote_cache.send(method, *args, &block)
57
+ end
58
+ end
59
+ end
data/lib/cash/lock.rb ADDED
@@ -0,0 +1,53 @@
1
+ module Cash
2
+ class Lock
3
+ class Error < RuntimeError; end
4
+
5
+ DEFAULT_RETRY = 5
6
+ DEFAULT_EXPIRY = 30
7
+
8
+ def initialize(cache)
9
+ @cache = cache
10
+ @runtime = Benchmark::Tms.new
11
+ end
12
+
13
+ def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
14
+ if recursive_lock?(key)
15
+ yield
16
+ else
17
+ acquire_lock(key, lock_expiry, retries)
18
+ begin
19
+ yield
20
+ ensure
21
+ release_lock(key)
22
+ end
23
+ end
24
+ end
25
+
26
+ def acquire_lock(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
27
+ retries.times do |count|
28
+ begin
29
+ response = @cache.add("lock/#{key}", Process.pid, lock_expiry)
30
+ return if response == "STORED\r\n"
31
+ raise Error if count == retries - 1
32
+ end
33
+ exponential_sleep(count) unless count == retries - 1
34
+ end
35
+ raise Error, "Couldn't acquire memcache lock for: #{key}"
36
+ end
37
+
38
+ def release_lock(key)
39
+ @cache.delete("lock/#{key}")
40
+ end
41
+
42
+ def exponential_sleep(count)
43
+ @runtime += Benchmark::measure { sleep((2**count) / 2.0) }
44
+ end
45
+
46
+ private
47
+
48
+ def recursive_lock?(key)
49
+ @cache.get("lock/#{key}") == Process.pid
50
+ end
51
+
52
+ end
53
+ 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
@@ -0,0 +1,173 @@
1
+ module Cash
2
+ module Query
3
+ class Abstract
4
+ delegate :with_exclusive_scope, :get, :table_name, :indices, :find_from_ids_without_cache, :cache_key, :columns_hash, :to => :@active_record
5
+
6
+ def self.perform(*args)
7
+ new(*args).perform
8
+ end
9
+
10
+ def initialize(active_record, options1, options2)
11
+ @active_record, @options1, @options2 = active_record, options1, options2 || {}
12
+
13
+ if active_record.base_class != active_record
14
+ @options2[:conditions] = active_record.merge_conditions(
15
+ @options2[:conditions], { active_record.inheritance_column => active_record.to_s }
16
+ )
17
+ end
18
+ end
19
+
20
+ def perform(find_options = {}, get_options = {})
21
+ if cache_config = cacheable?(@options1, @options2, find_options)
22
+ cache_keys, index = cache_keys(cache_config[0]), cache_config[1]
23
+
24
+ misses, missed_keys, objects = hit_or_miss(cache_keys, index, get_options)
25
+ format_results(cache_keys, choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects))
26
+ else
27
+ uncacheable
28
+ end
29
+ end
30
+
31
+ DESC = /DESC/i
32
+
33
+ def order
34
+ @order ||= begin
35
+ if order_sql = @options1[:order] || @options2[:order]
36
+ matched, table_name, column_name, direction = *(ORDER.match(order_sql))
37
+ [column_name, direction =~ DESC ? :desc : :asc]
38
+ else
39
+ ['id', :asc]
40
+ end
41
+ end
42
+ end
43
+
44
+ def limit
45
+ @limit ||= @options1[:limit] || @options2[:limit]
46
+ end
47
+
48
+ def offset
49
+ @offset ||= @options1[:offset] || @options2[:offset] || 0
50
+ end
51
+
52
+ def calculation?
53
+ false
54
+ end
55
+
56
+ private
57
+ def cacheable?(*optionss)
58
+ optionss.each { |options| return unless safe_options_for_cache?(options) }
59
+ partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
60
+ return if partial_indices.include?(nil)
61
+ attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
62
+
63
+ attribute_value_pairs.each do |attribute_value_pair|
64
+ return false if attribute_value_pair.last.is_a?(Array)
65
+ end
66
+
67
+ if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
68
+ if index.matches?(self)
69
+ [attribute_value_pairs, index]
70
+ end
71
+ end
72
+ end
73
+
74
+ def hit_or_miss(cache_keys, index, options)
75
+ misses, missed_keys = nil, nil
76
+ objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
77
+ misses = miss(missed_keys, @options1.merge(:limit => index.window))
78
+ serialize_objects(index, misses)
79
+ end
80
+ [misses, missed_keys, objects]
81
+ end
82
+
83
+ def cache_keys(attribute_value_pairs)
84
+ attribute_value_pairs.flatten.join('/')
85
+ end
86
+
87
+ def safe_options_for_cache?(options)
88
+ return false unless options.kind_of?(Hash)
89
+ options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
90
+ end
91
+
92
+ def attribute_value_pairs_for_conditions(conditions)
93
+ case conditions
94
+ when Hash
95
+ conditions.to_a.collect { |key, value| [key.to_s, value] }
96
+ when String
97
+ parse_indices_from_condition(conditions)
98
+ when Array
99
+ parse_indices_from_condition(*conditions)
100
+ when NilClass
101
+ []
102
+ end
103
+ end
104
+
105
+ AND = /\s+AND\s+/i
106
+ TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
107
+ VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
108
+ KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
109
+ ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
110
+
111
+ def parse_indices_from_condition(conditions = '', *values)
112
+ values = values.dup
113
+ conditions.split(AND).inject([]) do |indices, condition|
114
+ matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
115
+ if matched
116
+ value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
117
+ indices << [column_name, value]
118
+ else
119
+ return nil
120
+ end
121
+ end
122
+ end
123
+
124
+ def indexed_on?(attributes)
125
+ indices.detect { |index| index == attributes }
126
+ end
127
+ alias_method :index_for, :indexed_on?
128
+
129
+ def format_results(cache_keys, objects)
130
+ return objects if objects.blank?
131
+
132
+ objects = convert_to_array(cache_keys, objects)
133
+ objects = apply_limits_and_offsets(objects, @options1)
134
+ deserialize_objects(objects)
135
+ end
136
+
137
+ def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
138
+ missed_keys == cache_keys ? misses : objects
139
+ end
140
+
141
+ def serialize_objects(index, objects)
142
+ Array(objects).collect { |missed| index.serialize_object(missed) }
143
+ end
144
+
145
+ def convert_to_array(cache_keys, object)
146
+ if object.kind_of?(Hash)
147
+ cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
148
+ else
149
+ Array(object)
150
+ end
151
+ end
152
+
153
+ def apply_limits_and_offsets(results, options)
154
+ results.slice((options[:offset] || 0), (options[:limit] || results.length))
155
+ end
156
+
157
+ def deserialize_objects(objects)
158
+ if objects.first.kind_of?(ActiveRecord::Base)
159
+ objects
160
+ else
161
+ cache_keys = objects.collect { |id| "id/#{id}" }
162
+ objects = get(cache_keys, &method(:find_from_keys))
163
+ convert_to_array(cache_keys, objects)
164
+ end
165
+ end
166
+
167
+ def find_from_keys(*missing_keys)
168
+ missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
169
+ find_from_ids_without_cache(missing_ids, @options1)
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,45 @@
1
+ module Cash
2
+ module Query
3
+ class Calculation < Abstract
4
+ delegate :calculate_without_cache, :incr, :to => :@active_record
5
+
6
+ def initialize(active_record, operation, column, options1, options2)
7
+ super(active_record, options1, options2)
8
+ @operation, @column = operation, column
9
+ end
10
+
11
+ def perform
12
+ super({}, :raw => true)
13
+ end
14
+
15
+ def calculation?
16
+ true
17
+ end
18
+
19
+ protected
20
+ def miss(_, __)
21
+ calculate_without_cache(@operation, @column, @options1)
22
+ end
23
+
24
+ def uncacheable
25
+ calculate_without_cache(@operation, @column, @options1)
26
+ end
27
+
28
+ def format_results(_, objects)
29
+ objects.to_i
30
+ end
31
+
32
+ def serialize_objects(_, objects)
33
+ objects.to_s
34
+ end
35
+
36
+ def cacheable?(*optionss)
37
+ @column == :all && super(*optionss)
38
+ end
39
+
40
+ def cache_keys(attribute_value_pairs)
41
+ "#{super(attribute_value_pairs)}/#{@operation}"
42
+ end
43
+ end
44
+ end
45
+ end