factorylabs-cache-money 0.2.5

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,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,167 @@
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
+
20
+ describe "when the key size is very large" do
21
+ it "should hash the key to fit" do
22
+ k = "a" * 1024
23
+ Story.set(k, "foo")
24
+ Story.fetch(k).should == "foo"
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#fetch([...])', :shared => true do
30
+ describe 'when there is a total cache miss' do
31
+ it 'yields the keys to the block' do
32
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| ["doo", "doo"] }.should == {
33
+ "Story:1/yabba" => "doo",
34
+ "Story:1/dabba" => "doo"
35
+ }
36
+ end
37
+ end
38
+
39
+ describe 'when there is a partial cache miss' do
40
+ it 'yields just the missing ids to the block' do
41
+ Story.set("yabba", "dabba")
42
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| "doo" }.should == {
43
+ "Story:1/yabba" => "dabba",
44
+ "Story:1/dabba" => "doo"
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '#get' do
52
+ describe '#get("...")' do
53
+ describe 'when there is a cache miss' do
54
+ it 'returns the value of the block' do
55
+ Story.get("yabba") { "dabba" }.should == "dabba"
56
+ end
57
+
58
+ it 'adds to the cache' do
59
+ Story.get("yabba") { "dabba" }
60
+ Story.get("yabba").should == "dabba"
61
+ end
62
+ end
63
+
64
+ describe 'when there is a cache hit' do
65
+ before do
66
+ Story.set("yabba", "dabba")
67
+ end
68
+
69
+ it 'returns the value of the cache' do
70
+ Story.get("yabba") { "doo" }.should == "dabba"
71
+ end
72
+
73
+ it 'does nothing to the cache' do
74
+ Story.get("yabba") { "doo" }
75
+ Story.get("yabba").should == "dabba"
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '#get([...])' do
81
+ it_should_behave_like "#fetch([...])"
82
+ end
83
+ end
84
+
85
+ describe '#incr' do
86
+ describe 'when there is a cache hit' do
87
+ before do
88
+ Story.set("count", 0)
89
+ end
90
+
91
+ it 'increments the value of the cache' do
92
+ Story.incr("count", 2)
93
+ Story.get("count", :raw => true).should =~ /2/
94
+ end
95
+
96
+ it 'returns the new cache value' do
97
+ Story.incr("count", 2).should == 2
98
+ end
99
+ end
100
+
101
+ describe 'when there is a cache miss' do
102
+ it 'initializes the value of the cache to the value of the block' do
103
+ Story.incr("count", 1) { 5 }
104
+ Story.get("count", :raw => true).should =~ /5/
105
+ end
106
+
107
+ it 'returns the new cache value' do
108
+ Story.incr("count", 1) { 2 }.should == 2
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '#add' do
114
+ describe 'when the value already exists' do
115
+ it 'yields to the block' do
116
+ Story.set("count", 1)
117
+ Story.add("count", 1) { "yield me" }.should == "yield me"
118
+ end
119
+ end
120
+
121
+ describe 'when the value does not already exist' do
122
+ it 'adds the key to the cache' do
123
+ Story.add("count", 1)
124
+ Story.get("count").should == 1
125
+ end
126
+ end
127
+ end
128
+
129
+ describe '#decr' do
130
+ describe 'when there is a cache hit' do
131
+ before do
132
+ Story.incr("count", 1) { 10 }
133
+ end
134
+
135
+ it 'decrements the value of the cache' do
136
+ Story.decr("count", 2)
137
+ Story.get("count", :raw => true).should =~ /8/
138
+ end
139
+
140
+ it 'returns the new cache value' do
141
+ Story.decr("count", 2).should == 8
142
+ end
143
+ end
144
+
145
+ describe 'when there is a cache miss' do
146
+ it 'initializes the value of the cache to the value of the block' do
147
+ Story.decr("count", 1) { 5 }
148
+ Story.get("count", :raw => true).should =~ /5/
149
+ end
150
+
151
+ it 'returns the new cache value' do
152
+ Story.decr("count", 1) { 2 }.should == 2
153
+ end
154
+ end
155
+ end
156
+
157
+ describe '#cache_key' do
158
+ it 'uses the version number' do
159
+ Story.version 1
160
+ Story.cache_key("foo").should == "Story:1/foo"
161
+
162
+ Story.version 2
163
+ Story.cache_key("foo").should == "Story:2/foo"
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,221 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe Finders do
5
+ describe 'when the cache is populated' do
6
+ describe "#find" do
7
+ describe '#find(id...)' do
8
+ describe '#find(id)' do
9
+ it "returns an active record" do
10
+ story = Story.create!(:title => 'a story')
11
+ Story.find(story.id).should == story
12
+ end
13
+ end
14
+
15
+ describe 'when the object is destroyed' do
16
+ describe '#find(id)' do
17
+ it "raises an error" do
18
+ story = Story.create!(:title => "I am delicious")
19
+ story.destroy
20
+ lambda { Story.find(story.id) }.should raise_error(ActiveRecord::RecordNotFound)
21
+ end
22
+ end
23
+ end
24
+
25
+ describe '#find(id1, id2, ...)' do
26
+ it "returns an array" do
27
+ story1, story2 = Story.create!, Story.create!
28
+ Story.find(story1.id, story2.id).should == [story1, story2]
29
+ end
30
+
31
+ describe "#find(id, nil)" do
32
+ it "ignores the nils" do
33
+ story = Story.create!
34
+ Story.find(story.id, nil).should == story
35
+ end
36
+ end
37
+
38
+ describe "#find(id,id,id..)" do
39
+ it "accepts a larger than normal set of ids" do
40
+ st = []
41
+ 1.upto(250) do |i|
42
+ st << Story.create!
43
+ end
44
+ ids = st.collect(&:id)
45
+ Story.find(ids).should == st
46
+ end
47
+ end
48
+ end
49
+
50
+ describe 'when given nonexistent ids' do
51
+ describe 'when given one nonexistent id' do
52
+ it 'raises an error' do
53
+ lambda { Story.find(1) }.should raise_error(ActiveRecord::RecordNotFound)
54
+ end
55
+ end
56
+
57
+ describe 'when given multiple nonexistent ids' do
58
+ it "raises an error" do
59
+ lambda { Story.find(1, 2, 3) }.should raise_error(ActiveRecord::RecordNotFound)
60
+ end
61
+ end
62
+
63
+
64
+ describe '#find(nil)' do
65
+ it 'raises an error' do
66
+ lambda { Story.find(nil) }.should raise_error(ActiveRecord::RecordNotFound)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '#find(object)' do
73
+ it "coerces arguments to integers" do
74
+ story = Story.create!
75
+ Story.find(story.id.to_s).should == story
76
+ end
77
+ end
78
+
79
+ describe '#find([...])' do
80
+ describe 'when given an array with valid ids' do
81
+ it "#finds the object with that id" do
82
+ story = Story.create!
83
+ Story.find([story.id]).should == [story]
84
+ end
85
+ end
86
+
87
+ describe "when given a large array of ids" do
88
+ it "#finds all objects with those ids" do
89
+ st = []
90
+ 1.upto(250) do |i|
91
+ st << Story.create!
92
+ end
93
+ ids = st.collect(&:id)
94
+ Story.find(:all, :conditions => { :id => ids }).should == st
95
+ end
96
+ end
97
+
98
+ describe '#find([])' do
99
+ it 'returns the empty array' do
100
+ Story.find([]).should == []
101
+ end
102
+ end
103
+
104
+ describe 'when given nonexistent ids' do
105
+ it 'raises an error' do
106
+ lambda { Story.find([1, 2, 3]) }.should raise_error(ActiveRecord::RecordNotFound)
107
+ end
108
+ end
109
+
110
+ describe 'when given limits and offsets' do
111
+ describe '#find([1, 2, ...], :limit => ..., :offset => ...)' do
112
+ it "returns the correct slice of objects" do
113
+ character1 = Character.create!(:name => "Sam", :story_id => 1)
114
+ character2 = Character.create!(:name => "Sam", :story_id => 1)
115
+ character3 = Character.create!(:name => "Sam", :story_id => 1)
116
+ Character.find(
117
+ [character1.id, character2.id, character3.id],
118
+ :conditions => { :name => "Sam", :story_id => 1 }, :limit => 2
119
+ ).should == [character1, character2]
120
+ end
121
+ end
122
+
123
+ describe '#find([1], :limit => 0)' do
124
+ it "raises an error" do
125
+ character = Character.create!(:name => "Sam", :story_id => 1)
126
+ lambda do
127
+ Character.find([character.id], :conditions => { :name => "Sam", :story_id => 1 }, :limit => 0)
128
+ end.should raise_error(ActiveRecord::RecordNotFound)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ describe '#find(:first, ..., :offset => ...)' do
135
+ it "#finds the object in the correct order" do
136
+ story1 = Story.create!(:title => 'title1')
137
+ story2 = Story.create!(:title => story1.title)
138
+ Story.find(:first, :conditions => { :title => story1.title }, :offset => 1).should == story2
139
+ end
140
+ end
141
+
142
+ describe '#find(:first, :conditions => [])' do
143
+ it 'works' do
144
+ story = Story.create!
145
+ Story.find(:first, :conditions => []).should == story
146
+ end
147
+ end
148
+
149
+ describe "#find(:first, :conditions => '...')" do
150
+ it "uses the active record instance to typecast values extracted from the conditions" do
151
+ story1 = Story.create! :title => 'a story', :published => true
152
+ story2 = Story.create! :title => 'another story', :published => false
153
+ Story.get('published/false').should == [story2.id]
154
+ Story.find(:first, :conditions => 'published = 0').should == story2
155
+ end
156
+ end
157
+ end
158
+
159
+ describe '#find_by_attr' do
160
+ describe '#find_by_attr(nil)' do
161
+ it 'returns nil' do
162
+ Story.find_by_id(nil).should == nil
163
+ end
164
+ end
165
+
166
+ describe 'when given non-existent ids' do
167
+ it 'returns nil' do
168
+ Story.find_by_id(-1).should == nil
169
+ end
170
+ end
171
+ end
172
+
173
+ describe '#find_all_by_attr' do
174
+ describe 'when given non-existent ids' do
175
+ it "does not raise an error" do
176
+ lambda { Story.find_all_by_id([-1, -2, -3]) }.should_not raise_error
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ describe 'when the cache is partially populated' do
183
+ describe '#find(:all, :conditions => ...)' do
184
+ it "returns the correct records" do
185
+ story1 = Story.create!(:title => title = 'once upon a time...')
186
+ $memcache.flush_all
187
+ story2 = Story.create!(:title => title)
188
+ Story.find(:all, :conditions => { :title => story1.title }).should == [story1, story2]
189
+ end
190
+ end
191
+
192
+ describe '#find(id1, id2, ...)' do
193
+ it "returns the correct records" do
194
+ story1 = Story.create!(:title => 'story 1')
195
+ $memcache.flush_all
196
+ story2 = Story.create!(:title => 'story 2')
197
+ Story.find(story1.id, story2.id).should == [story1, story2]
198
+ end
199
+ end
200
+ end
201
+
202
+ describe 'when the cache is not populated' do
203
+ describe '#find(id)' do
204
+ it "returns the correct records" do
205
+ story = Story.create!(:title => 'a story')
206
+ $memcache.flush_all
207
+ Story.find(story.id).should == story
208
+ end
209
+ end
210
+
211
+ describe '#find(id1, id2, ...)' do
212
+ it "handles finds with multiple ids correctly" do
213
+ story1 = Story.create!
214
+ story2 = Story.create!
215
+ $memcache.flush_all
216
+ Story.find(story1.id, story2.id).should == [story1, story2]
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,67 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe Finders do
5
+ describe 'Calculations' do
6
+ describe 'when the cache is populated' do
7
+ before do
8
+ @stories = [Story.create!(:title => @title = 'asdf'), Story.create!(:title => @title)]
9
+ end
10
+
11
+ describe '#count(:all, :conditions => ...)' do
12
+ it "does not use the database" do
13
+ Story.count(:all, :conditions => { :title => @title }).should == @stories.size
14
+ end
15
+ end
16
+
17
+ describe '#count(:column, :conditions => ...)' do
18
+ it "uses the database, not the cache" do
19
+ mock(Story).get.never
20
+ Story.count(:title, :conditions => { :title => @title }).should == @stories.size
21
+ end
22
+ end
23
+
24
+ describe '#count(:all, :distinct => ..., :select => ...)' do
25
+ it 'uses the database, not the cache' do
26
+ mock(Story).get.never
27
+ Story.count(:all, :distinct => true, :select => :title, :conditions => { :title => @title }).should == @stories.collect(&:title).uniq.size
28
+ end
29
+ end
30
+
31
+ describe 'association proxies' do
32
+ describe '#count(:all, :conditions => ...)' do
33
+ it 'does not use the database' do
34
+ story = Story.create!
35
+ characters = [story.characters.create!(:name => name = 'name'), story.characters.create!(:name => name)]
36
+ mock(Story.connection).execute.never
37
+ story.characters.count(:all, :conditions => { :name => name }).should == characters.size
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ describe 'when the cache is not populated' do
44
+ describe '#count(:all, ...)' do
45
+ describe '#count(:all)' do
46
+ it 'uses the database, not the cache' do
47
+ mock(Story).get.never
48
+ Story.count
49
+ end
50
+ end
51
+
52
+ describe '#count(:all, :conditions => ...)' do
53
+ before do
54
+ Story.create!(:title => @title = 'title')
55
+ $memcache.flush_all
56
+ end
57
+
58
+ it "populates the count correctly" do
59
+ Story.count(:all, :conditions => { :title => @title }).should == 1
60
+ Story.fetch("title/#{@title}/count", :raw => true).should =~ /\s*1\s*/
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end