nkallen-cache-money 0.2.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/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, :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 : 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