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