ngmoco-cache-money 0.2.9

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,51 @@
1
+ module Cash
2
+ class Lock
3
+ class Error < RuntimeError; end
4
+
5
+ INITIAL_WAIT = 1
6
+ DEFAULT_RETRY = 5
7
+ DEFAULT_EXPIRY = 30
8
+
9
+ def initialize(cache)
10
+ @cache = cache
11
+ end
12
+
13
+ def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY, initial_wait = INITIAL_WAIT)
14
+ if recursive_lock?(key)
15
+ yield
16
+ else
17
+ acquire_lock(key, lock_expiry, retries, initial_wait)
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, initial_wait = INITIAL_WAIT)
27
+ retries.times do |count|
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
+ exponential_sleep(count, initial_wait) unless count == retries - 1
32
+ end
33
+ raise Error, "Couldn't acquire memcache lock for: #{key}"
34
+ end
35
+
36
+ def release_lock(key)
37
+ @cache.delete("lock/#{key}")
38
+ end
39
+
40
+ def exponential_sleep(count, initial_wait)
41
+ sleep((2**count) / initial_wait)
42
+ end
43
+
44
+ private
45
+
46
+ def recursive_lock?(key)
47
+ @cache.get("lock/#{key}") == Process.pid
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,181 @@
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("---- UNCACHEABLE #{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
+ optionss.each { |options| return unless safe_options_for_cache?(options) }
65
+ partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
66
+ return if partial_indices.include?(nil)
67
+ attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
68
+
69
+ # attribute_value_pairs.each do |attribute_value_pair|
70
+ # return false if attribute_value_pair.last.is_a?(Array)
71
+ # end
72
+
73
+ if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
74
+ if index.matches?(self)
75
+ [attribute_value_pairs, index]
76
+ end
77
+ end
78
+ end
79
+
80
+ def hit_or_miss(cache_keys, index, options)
81
+ misses, missed_keys = nil, nil
82
+ objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
83
+ misses = miss(missed_keys, @options1.merge(:limit => index.window))
84
+ serialize_objects(index, misses)
85
+ end
86
+ [misses, missed_keys, objects]
87
+ end
88
+
89
+ def cache_keys(attribute_value_pairs)
90
+ attribute_value_pairs.flatten.join('/')
91
+ end
92
+
93
+ def safe_options_for_cache?(options)
94
+ return false unless options.kind_of?(Hash)
95
+ options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
96
+ end
97
+
98
+ def attribute_value_pairs_for_conditions(conditions)
99
+ case conditions
100
+ when Hash
101
+ conditions.to_a.collect { |key, value| [key.to_s, value] }
102
+ when String
103
+ parse_indices_from_condition(conditions.gsub('1 = 1 AND ', '')) #ignore unnecessary conditions
104
+ when Array
105
+ parse_indices_from_condition(*conditions)
106
+ when NilClass
107
+ []
108
+ end
109
+ end
110
+
111
+ AND = /\s+AND\s+/i
112
+ TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
113
+ VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
114
+ KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
115
+ ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
116
+
117
+ def parse_indices_from_condition(conditions = '', *values)
118
+ values = values.dup
119
+ conditions.split(AND).inject([]) do |indices, condition|
120
+ matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
121
+ if matched
122
+ value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
123
+ indices << [column_name, value]
124
+ else
125
+ return nil
126
+ end
127
+ end
128
+ end
129
+
130
+ def indexed_on?(attributes)
131
+ indices.detect { |index| index == attributes }
132
+ rescue NoMethodError
133
+ nil
134
+ end
135
+ alias_method :index_for, :indexed_on?
136
+
137
+ def format_results(cache_keys, objects)
138
+ return objects if objects.blank?
139
+
140
+ objects = convert_to_array(cache_keys, objects)
141
+ objects = apply_limits_and_offsets(objects, @options1)
142
+ deserialize_objects(objects)
143
+ end
144
+
145
+ def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
146
+ missed_keys == cache_keys ? misses : objects
147
+ end
148
+
149
+ def serialize_objects(index, objects)
150
+ Array(objects).collect { |missed| index.serialize_object(missed) }
151
+ end
152
+
153
+ def convert_to_array(cache_keys, object)
154
+ if object.kind_of?(Hash)
155
+ cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
156
+ else
157
+ Array(object)
158
+ end
159
+ end
160
+
161
+ def apply_limits_and_offsets(results, options)
162
+ results.slice((options[:offset] || 0), (options[:limit] || results.length))
163
+ end
164
+
165
+ def deserialize_objects(objects)
166
+ if objects.first.kind_of?(ActiveRecord::Base)
167
+ objects
168
+ else
169
+ cache_keys = objects.collect { |id| "id/#{id}" }
170
+ objects = get(cache_keys, &method(:find_from_keys))
171
+ convert_to_array(cache_keys, objects)
172
+ end
173
+ end
174
+
175
+ def find_from_keys(*missing_keys)
176
+ missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
177
+ find_from_ids_without_cache(missing_ids, @options1)
178
+ end
179
+ end
180
+ end
181
+ 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
@@ -0,0 +1,42 @@
1
+ module Cash
2
+ class Transactional
3
+ attr_reader :memcache
4
+
5
+ def initialize(memcache, lock)
6
+ @memcache, @cache = [memcache, memcache]
7
+ @lock = lock
8
+ end
9
+
10
+ def transaction
11
+ exception_was_raised = false
12
+ begin_transaction
13
+ result = yield
14
+ rescue Object => e
15
+ exception_was_raised = true
16
+ raise
17
+ ensure
18
+ begin
19
+ @cache.flush unless exception_was_raised
20
+ ensure
21
+ end_transaction
22
+ end
23
+ end
24
+
25
+ def method_missing(method, *args, &block)
26
+ @cache.send(method, *args, &block)
27
+ end
28
+
29
+ def respond_to?(method)
30
+ @cache.respond_to?(method)
31
+ end
32
+
33
+ private
34
+ def begin_transaction
35
+ @cache = Buffered.push(@cache, @lock)
36
+ end
37
+
38
+ def end_transaction
39
+ @cache = @cache.pop
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ class Array
2
+ alias_method :count, :size
3
+
4
+ def to_hash_without_nils
5
+ keys_and_values_without_nils = reject { |key, value| value.nil? }
6
+ shallow_flattened_keys_and_values_without_nils = keys_and_values_without_nils.inject([]) { |result, pair| result += pair }
7
+ Hash[*shallow_flattened_keys_and_values_without_nils]
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module Marshal
2
+ class << self
3
+ def constantize(name)
4
+ name.constantize
5
+ end
6
+
7
+ def load_with_constantize(value)
8
+ begin
9
+ Marshal.load_without_constantize value
10
+ rescue ArgumentError => e
11
+ _, class_name = *(/undefined class\/module ([\w:]*\w)/.match(e.message))
12
+ raise if !class_name
13
+ constantize(class_name)
14
+ Marshal.load value
15
+ end
16
+ end
17
+ alias_method_chain :load, :constantize
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ module Cash
2
+ module WriteThrough
3
+ DEFAULT_TTL = 12.hours
4
+
5
+ def self.included(active_record_class)
6
+ active_record_class.class_eval do
7
+ include InstanceMethods
8
+ extend ClassMethods
9
+ end
10
+ end
11
+
12
+ module InstanceMethods
13
+ def self.included(active_record_class)
14
+ active_record_class.class_eval do
15
+ after_create :add_to_caches
16
+ after_update :update_caches
17
+ after_destroy :remove_from_caches
18
+ end
19
+ end
20
+
21
+ def add_to_caches
22
+ InstanceMethods.unfold(self.class, :add_to_caches, self)
23
+ end
24
+
25
+ def update_caches
26
+ InstanceMethods.unfold(self.class, :update_caches, self)
27
+ end
28
+
29
+ def remove_from_caches
30
+ return if new_record?
31
+ InstanceMethods.unfold(self.class, :remove_from_caches, self)
32
+ end
33
+
34
+ def expire_caches
35
+ InstanceMethods.unfold(self.class, :expire_caches, self)
36
+ end
37
+
38
+ def shallow_clone
39
+ clone = self.class.new
40
+ clone.instance_variable_set("@attributes", instance_variable_get(:@attributes))
41
+ clone.instance_variable_set("@new_record", new_record?)
42
+ clone
43
+ end
44
+
45
+ private
46
+ def self.unfold(klass, operation, object)
47
+ while klass < ActiveRecord::Base && klass.ancestors.include?(WriteThrough)
48
+ klass.send(operation, object)
49
+ klass = klass.superclass
50
+ end
51
+ end
52
+ end
53
+
54
+ module ClassMethods
55
+ def add_to_caches(object)
56
+ indices.each { |index| index.add(object) }
57
+ end
58
+
59
+ def update_caches(object)
60
+ indices.each { |index| index.update(object) }
61
+ end
62
+
63
+ def remove_from_caches(object)
64
+ indices.each { |index| index.remove(object) }
65
+ end
66
+
67
+ def expire_caches(object)
68
+ indices.each { |index| index.delete(object) }
69
+ end
70
+ end
71
+ end
72
+ end