ashleym1972-cache-money 0.2.5.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.
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