mguymon-cache-money 0.2.12

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.
@@ -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
@@ -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
@@ -0,0 +1,194 @@
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, :logger, :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 @options2.empty? and active_record.base_class != active_record
14
+ # @options2 = { :conditions => { active_record.inheritance_column => active_record.to_s }}
15
+ # end
16
+ # if active_record.base_class != active_record
17
+ # @options2[:conditions] = active_record.merge_conditions(
18
+ # @options2[:conditions], { active_record.inheritance_column => active_record.to_s }
19
+ # )
20
+ # end
21
+ end
22
+
23
+ def perform(find_options = {}, get_options = {})
24
+ if cache_config = cacheable?(@options1, @options2, find_options)
25
+ cache_keys, index = cache_keys(cache_config[0]), cache_config[1]
26
+
27
+ misses, missed_keys, objects = hit_or_miss(cache_keys, index, get_options)
28
+ format_results(cache_keys, choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects))
29
+ else
30
+ logger.debug(" \e[1;4;31mUNCACHEABLE\e[0m #{table_name} - #{find_options.inspect} - #{get_options.inspect} - #{@options1.inspect} - #{@options2.inspect}") if logger
31
+ uncacheable
32
+ end
33
+ end
34
+
35
+ DESC = /DESC/i
36
+
37
+ def order
38
+ @order ||= begin
39
+ if order_sql = @options1[:order] || @options2[:order]
40
+ matched, table_name, column_name, direction = *(ORDER.match(order_sql.to_s))
41
+ [column_name, direction =~ DESC ? :desc : :asc]
42
+ else
43
+ ['id', :asc]
44
+ end
45
+ end
46
+ rescue TypeError
47
+ ['id', :asc]
48
+ end
49
+
50
+ def limit
51
+ @limit ||= @options1[:limit] || @options2[:limit]
52
+ end
53
+
54
+ def offset
55
+ @offset ||= @options1[:offset] || @options2[:offset] || 0
56
+ end
57
+
58
+ def calculation?
59
+ false
60
+ end
61
+
62
+ private
63
+ def cacheable?(*optionss)
64
+ return false if @active_record.respond_to?(:cachable?) && ! @active_record.cachable?(*optionss)
65
+ optionss.each { |options| return unless safe_options_for_cache?(options) }
66
+ partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
67
+ return if partial_indices.include?(nil)
68
+ attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
69
+
70
+ # attribute_value_pairs.each do |attribute_value_pair|
71
+ # return false if attribute_value_pair.last.is_a?(Array)
72
+ # end
73
+
74
+ if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
75
+ if index.matches?(self)
76
+ [attribute_value_pairs, index]
77
+ end
78
+ end
79
+ end
80
+
81
+ def hit_or_miss(cache_keys, index, options)
82
+ misses, missed_keys = nil, nil
83
+ objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
84
+ misses = miss(missed_keys, @options1.merge(:limit => index.window))
85
+ serialize_objects(index, misses)
86
+ end
87
+ [misses, missed_keys, objects]
88
+ end
89
+
90
+ def cache_keys(attribute_value_pairs)
91
+ attribute_value_pairs.flatten.join('/')
92
+ end
93
+
94
+ def safe_options_for_cache?(options)
95
+ return false unless options.kind_of?(Hash)
96
+ options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
97
+ end
98
+
99
+ def attribute_value_pairs_for_conditions(conditions)
100
+ case conditions
101
+ when Hash
102
+ conditions.to_a.collect { |key, value| [key.to_s, value] }
103
+ when String
104
+ parse_indices_from_condition(conditions.gsub('1 = 1 AND ', '')) #ignore unnecessary conditions
105
+ when Array
106
+ parse_indices_from_condition(*conditions)
107
+ when NilClass
108
+ []
109
+ end
110
+ end
111
+
112
+ AND = /\s+AND\s+/i
113
+ TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
114
+ VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
115
+ KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
116
+ ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
117
+
118
+ def parse_indices_from_condition(conditions = '', *values)
119
+ values = values.dup
120
+ conditions.split(AND).inject([]) do |indices, condition|
121
+ matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
122
+ if matched
123
+ # value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
124
+ if sql_value == '?'
125
+ value = values.shift
126
+ else
127
+ column = columns_hash[column_name]
128
+ raise "could not find column #{column_name} in columns #{columns_hash.keys.join(',')}" if column.nil?
129
+ if sql_value[0..0] == ':' && values && values.count > 0 && values[0].is_a?(Hash)
130
+ symb = sql_value[1..-1].to_sym
131
+ value = column.type_cast(values[0][symb])
132
+ else
133
+ value = column.type_cast(sql_value)
134
+ end
135
+ end
136
+ indices << [column_name, value]
137
+ else
138
+ return nil
139
+ end
140
+ end
141
+ end
142
+
143
+ def indexed_on?(attributes)
144
+ indices.detect { |index| index == attributes }
145
+ rescue NoMethodError
146
+ nil
147
+ end
148
+ alias_method :index_for, :indexed_on?
149
+
150
+ def format_results(cache_keys, objects)
151
+ return objects if objects.blank?
152
+
153
+ objects = convert_to_array(cache_keys, objects)
154
+ objects = apply_limits_and_offsets(objects, @options1)
155
+ deserialize_objects(objects)
156
+ end
157
+
158
+ def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
159
+ missed_keys == cache_keys ? misses : objects
160
+ end
161
+
162
+ def serialize_objects(index, objects)
163
+ Array(objects).collect { |missed| index.serialize_object(missed) }
164
+ end
165
+
166
+ def convert_to_array(cache_keys, object)
167
+ if object.kind_of?(Hash)
168
+ cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
169
+ else
170
+ Array(object)
171
+ end
172
+ end
173
+
174
+ def apply_limits_and_offsets(results, options)
175
+ results.slice((options[:offset] || 0), (options[:limit] || results.length))
176
+ end
177
+
178
+ def deserialize_objects(objects)
179
+ if objects.first.kind_of?(ActiveRecord::Base)
180
+ objects
181
+ else
182
+ cache_keys = objects.collect { |id| "id/#{id}" }
183
+ objects = get(cache_keys, &method(:find_from_keys))
184
+ convert_to_array(cache_keys, objects)
185
+ end
186
+ end
187
+
188
+ def find_from_keys(*missing_keys)
189
+ missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
190
+ find_from_ids_without_cache(missing_ids, {})
191
+ end
192
+ end
193
+ end
194
+ 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,50 @@
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
+ def miss(missing_keys, options)
30
+ find_from_keys(*missing_keys)
31
+ end
32
+
33
+ def uncacheable
34
+ find_from_ids_without_cache(@original_ids, @options1)
35
+ end
36
+
37
+ private
38
+ def convert_to_active_record_collection(objects)
39
+ case objects.size
40
+ when 0
41
+ raise ActiveRecord::RecordNotFound
42
+ when 1
43
+ @expects_array ? objects : objects.first
44
+ else
45
+ objects
46
+ end
47
+ end
48
+ end
49
+ end
50
+ 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