cache-money 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
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,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
@@ -0,0 +1,159 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe Accessor do
5
+ describe '#fetch' do
6
+ describe '#fetch("...")' do
7
+ describe 'when there is a cache miss' do
8
+ it 'returns nil' do
9
+ Story.fetch("yabba").should be_nil
10
+ end
11
+ end
12
+
13
+ describe 'when there is a cache hit' do
14
+ it 'returns the value of the cache' do
15
+ Story.set("yabba", "dabba")
16
+ Story.fetch("yabba").should == "dabba"
17
+ end
18
+ end
19
+ end
20
+
21
+ describe '#fetch([...])', :shared => true do
22
+ describe 'when there is a total cache miss' do
23
+ it 'yields the keys to the block' do
24
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| ["doo", "doo"] }.should == {
25
+ "Story:1/yabba" => "doo",
26
+ "Story:1/dabba" => "doo"
27
+ }
28
+ end
29
+ end
30
+
31
+ describe 'when there is a partial cache miss' do
32
+ it 'yields just the missing ids to the block' do
33
+ Story.set("yabba", "dabba")
34
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| "doo" }.should == {
35
+ "Story:1/yabba" => "dabba",
36
+ "Story:1/dabba" => "doo"
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ describe '#get' do
44
+ describe '#get("...")' do
45
+ describe 'when there is a cache miss' do
46
+ it 'returns the value of the block' do
47
+ Story.get("yabba") { "dabba" }.should == "dabba"
48
+ end
49
+
50
+ it 'adds to the cache' do
51
+ Story.get("yabba") { "dabba" }
52
+ Story.get("yabba").should == "dabba"
53
+ end
54
+ end
55
+
56
+ describe 'when there is a cache hit' do
57
+ before do
58
+ Story.set("yabba", "dabba")
59
+ end
60
+
61
+ it 'returns the value of the cache' do
62
+ Story.get("yabba") { "doo" }.should == "dabba"
63
+ end
64
+
65
+ it 'does nothing to the cache' do
66
+ Story.get("yabba") { "doo" }
67
+ Story.get("yabba").should == "dabba"
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '#get([...])' do
73
+ it_should_behave_like "#fetch([...])"
74
+ end
75
+ end
76
+
77
+ describe '#incr' do
78
+ describe 'when there is a cache hit' do
79
+ before do
80
+ Story.set("count", 0)
81
+ end
82
+
83
+ it 'increments the value of the cache' do
84
+ Story.incr("count", 2)
85
+ Story.get("count", :raw => true).should =~ /2/
86
+ end
87
+
88
+ it 'returns the new cache value' do
89
+ Story.incr("count", 2).should == 2
90
+ end
91
+ end
92
+
93
+ describe 'when there is a cache miss' do
94
+ it 'initializes the value of the cache to the value of the block' do
95
+ Story.incr("count", 1) { 5 }
96
+ Story.get("count", :raw => true).should =~ /5/
97
+ end
98
+
99
+ it 'returns the new cache value' do
100
+ Story.incr("count", 1) { 2 }.should == 2
101
+ end
102
+ end
103
+ end
104
+
105
+ describe '#add' do
106
+ describe 'when the value already exists' do
107
+ it 'yields to the block' do
108
+ Story.set("count", 1)
109
+ Story.add("count", 1) { "yield me" }.should == "yield me"
110
+ end
111
+ end
112
+
113
+ describe 'when the value does not already exist' do
114
+ it 'adds the key to the cache' do
115
+ Story.add("count", 1)
116
+ Story.get("count").should == 1
117
+ end
118
+ end
119
+ end
120
+
121
+ describe '#decr' do
122
+ describe 'when there is a cache hit' do
123
+ before do
124
+ Story.incr("count", 1) { 10 }
125
+ end
126
+
127
+ it 'decrements the value of the cache' do
128
+ Story.decr("count", 2)
129
+ Story.get("count", :raw => true).should =~ /8/
130
+ end
131
+
132
+ it 'returns the new cache value' do
133
+ Story.decr("count", 2).should == 8
134
+ end
135
+ end
136
+
137
+ describe 'when there is a cache miss' do
138
+ it 'initializes the value of the cache to the value of the block' do
139
+ Story.decr("count", 1) { 5 }
140
+ Story.get("count", :raw => true).should =~ /5/
141
+ end
142
+
143
+ it 'returns the new cache value' do
144
+ Story.decr("count", 1) { 2 }.should == 2
145
+ end
146
+ end
147
+ end
148
+
149
+ describe '#cache_key' do
150
+ it 'uses the version number' do
151
+ Story.version 1
152
+ Story.cache_key("foo").should == "Story:1/foo"
153
+
154
+ Story.version 2
155
+ Story.cache_key("foo").should == "Story:2/foo"
156
+ end
157
+ end
158
+ end
159
+ end