record-cache 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/record-cache.rb +1 -0
- data/lib/record_cache/active_record.rb +318 -0
- data/lib/record_cache/base.rb +136 -0
- data/lib/record_cache/dispatcher.rb +90 -0
- data/lib/record_cache/multi_read.rb +51 -0
- data/lib/record_cache/query.rb +85 -0
- data/lib/record_cache/statistics.rb +82 -0
- data/lib/record_cache/strategy/base.rb +154 -0
- data/lib/record_cache/strategy/id_cache.rb +93 -0
- data/lib/record_cache/strategy/index_cache.rb +122 -0
- data/lib/record_cache/strategy/request_cache.rb +49 -0
- data/lib/record_cache/test/resettable_version_store.rb +49 -0
- data/lib/record_cache/version.rb +5 -0
- data/lib/record_cache/version_store.rb +54 -0
- data/lib/record_cache.rb +11 -0
- data/spec/db/database.yml +6 -0
- data/spec/db/schema.rb +42 -0
- data/spec/db/seeds.rb +40 -0
- data/spec/initializers/record_cache.rb +14 -0
- data/spec/lib/dispatcher_spec.rb +86 -0
- data/spec/lib/multi_read_spec.rb +51 -0
- data/spec/lib/query_spec.rb +148 -0
- data/spec/lib/statistics_spec.rb +140 -0
- data/spec/lib/strategy/base_spec.rb +241 -0
- data/spec/lib/strategy/id_cache_spec.rb +168 -0
- data/spec/lib/strategy/index_cache_spec.rb +223 -0
- data/spec/lib/strategy/request_cache_spec.rb +85 -0
- data/spec/lib/version_store_spec.rb +104 -0
- data/spec/models/apple.rb +8 -0
- data/spec/models/banana.rb +8 -0
- data/spec/models/pear.rb +6 -0
- data/spec/models/person.rb +11 -0
- data/spec/models/store.rb +13 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/after_commit.rb +71 -0
- data/spec/support/matchers/hit_cache_matcher.rb +53 -0
- data/spec/support/matchers/miss_cache_matcher.rb +53 -0
- data/spec/support/matchers/use_cache_matcher.rb +53 -0
- metadata +253 -0
@@ -0,0 +1,241 @@
|
|
1
|
+
$KCODE = 'UTF8'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe RecordCache::Strategy::Base do
|
5
|
+
|
6
|
+
it "should provide easy access to the Version Store" do
|
7
|
+
Apple.record_cache[:id].send(:version_store).should == RecordCache::Base.version_store
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should provide easy access to the Record Store" do
|
11
|
+
Apple.record_cache[:id].send(:record_store).should == RecordCache::Base.stores[:shared]
|
12
|
+
Banana.record_cache[:id].send(:record_store).should == RecordCache::Base.stores[:local]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should provide easy access to the statistics" do
|
16
|
+
Apple.record_cache[:person_id].send(:statistics).should == RecordCache::Statistics.find(Apple, :person_id)
|
17
|
+
Banana.record_cache[:id].send(:statistics).should == RecordCache::Statistics.find(Banana, :id)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should retrieve the cache key based on the :key option" do
|
21
|
+
Apple.record_cache[:id].send(:cache_key, 1).should == "rc/apl/1"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should retrieve the cache key based on the model name" do
|
25
|
+
Banana.record_cache[:id].send(:cache_key, 1).should == "rc/Banana/1"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should define the versioned key" do
|
29
|
+
Banana.record_cache[:id].send(:versioned_key, "rc/Banana/1", 2312423).should == "rc/Banana/1v2312423"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should serialize a record (currently Active Record only)" do
|
33
|
+
Banana.record_cache[:id].send(:serialize, Banana.find(1)).should == {:a=>{"name"=>"Blue Banana 1", "id"=>1, "store_id"=>2, "person_id"=>4}, :c=>"Banana"}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should deserialize a record (currently Active Record only)" do
|
37
|
+
Banana.record_cache[:id].send(:deserialize, {:a=>{"name"=>"Blue Banana 1", "id"=>1, "store_id"=>2, "person_id"=>4}, :c=>"Banana"}).should == Banana.find(1)
|
38
|
+
end
|
39
|
+
|
40
|
+
context "filter" do
|
41
|
+
it "should apply filter on :id cache hits" do
|
42
|
+
lambda{ @apples = Apple.where(:id => [1,2]).where(:name => "Adams Apple 1").all }.should use_cache(Apple).on(:id)
|
43
|
+
@apples.should == [Apple.find_by_name("Adams Apple 1")]
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should apply filter on index cache hits" do
|
47
|
+
lambda{ @apples = Apple.where(:store_id => 1).where(:name => "Adams Apple 1").all }.should use_cache(Apple).on(:store_id)
|
48
|
+
@apples.should == [Apple.find_by_name("Adams Apple 1")]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should return empty array when filter does not match any record" do
|
52
|
+
lambda{ @apples = Apple.where(:store_id => 1).where(:name => "Adams Apple Pie").all }.should use_cache(Apple).on(:store_id)
|
53
|
+
@apples.should == []
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should filter on text" do
|
57
|
+
lambda{ @apples = Apple.where(:id => [1,2]).where(:name => "Adams Apple 1").all }.should use_cache(Apple).on(:id)
|
58
|
+
@apples.should == [Apple.find_by_name("Adams Apple 1")]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should filter on integers" do
|
62
|
+
lambda{ @apples = Apple.where(:id => [1,2,8,9]).where(:store_id => 2).all }.should use_cache(Apple).on(:id)
|
63
|
+
@apples.map(&:id).sort.should == [8,9]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should filter on dates" do
|
67
|
+
lambda{ @people = Person.where(:id => [1,2,3]).where(:birthday => Date.civil(1953,11,11)).all }.should use_cache(Person).on(:id)
|
68
|
+
@people.size.should == 1
|
69
|
+
@people.first.name.should == "Blue"
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should filter on floats" do
|
73
|
+
lambda{ @people = Person.where(:id => [1,2,3]).where(:height => 1.75).all }.should use_cache(Person).on(:id)
|
74
|
+
@people.size.should == 2
|
75
|
+
@people.map(&:name).sort.should == ["Blue", "Cris"]
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should filter on arrays" do
|
79
|
+
lambda{ @apples = Apple.where(:id => [1,2,8,9]).where(:store_id => [2, 4]).all }.should use_cache(Apple).on(:id)
|
80
|
+
@apples.map(&:id).sort.should == [8,9]
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should filter on multiple fields" do
|
84
|
+
# make sure two apples exist with the same name
|
85
|
+
@apple = Apple.find(8)
|
86
|
+
@apple.name = Apple.find(9).name
|
87
|
+
@apple.save!
|
88
|
+
|
89
|
+
lambda{ @apples = Apple.where(:id => [1,2,3,8,9,10]).where(:store_id => 2).where(:name => @apple.name).all }.should use_cache(Apple).on(:id)
|
90
|
+
@apples.size.should == 2
|
91
|
+
@apples.map(&:name).should == [@apple.name, @apple.name]
|
92
|
+
@apples.map(&:id).sort.should == [8,9]
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
context "sort" do
|
98
|
+
it "should apply sort on :id cache hits" do
|
99
|
+
lambda{ @people = Person.where(:id => [1,2,3]).order("name DESC").all }.should use_cache(Person).on(:id)
|
100
|
+
@people.map(&:name).should == ["Cris", "Blue", "Adam"]
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should apply sort on index cache hits" do
|
104
|
+
lambda{ @apples = Apple.where(:store_id => 1).order("person_id ASC").all }.should use_cache(Apple).on(:store_id)
|
105
|
+
@apples.map(&:person_id).should == [nil, nil, 4, 4, 5]
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should default to ASC" do
|
109
|
+
lambda{ @apples = Apple.where(:store_id => 1).order("person_id").all }.should use_cache(Apple).on(:store_id)
|
110
|
+
@apples.map(&:person_id).should == [nil, nil, 4, 4, 5]
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should apply sort nil first for ASC" do
|
114
|
+
lambda{ @apples = Apple.where(:store_id => 1).order("person_id ASC").all }.should use_cache(Apple).on(:store_id)
|
115
|
+
@apples.map(&:person_id).should == [nil, nil, 4, 4, 5]
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should apply sort nil last for DESC" do
|
119
|
+
lambda{ @apples = Apple.where(:store_id => 1).order("person_id DESC").all }.should use_cache(Apple).on(:store_id)
|
120
|
+
@apples.map(&:person_id).should == [5, 4, 4, nil, nil]
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should sort ascending on text" do
|
124
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("name ASC").all }.should use_cache(Person).on(:id)
|
125
|
+
@people.map(&:name).should == ["Adam", "Blue", "Cris", "Fry"]
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should sort descending on text" do
|
129
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("name DESC").all }.should use_cache(Person).on(:id)
|
130
|
+
@people.map(&:name).should == ["Fry", "Cris", "Blue", "Adam"]
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should sort ascending on integers" do
|
134
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("id ASC").all }.should use_cache(Person).on(:id)
|
135
|
+
@people.map(&:id).should == [1,2,3,4]
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should sort descending on integers" do
|
139
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("id DESC").all }.should use_cache(Person).on(:id)
|
140
|
+
@people.map(&:id).should == [4,3,2,1]
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should sort ascending on dates" do
|
144
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("birthday ASC").all }.should use_cache(Person).on(:id)
|
145
|
+
@people.map(&:birthday).should == [Date.civil(1953,11,11), Date.civil(1975,03,20), Date.civil(1975,03,20), Date.civil(1985,01,20)]
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should sort descending on dates" do
|
149
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("birthday DESC").all }.should use_cache(Person).on(:id)
|
150
|
+
@people.map(&:birthday).should == [Date.civil(1985,01,20), Date.civil(1975,03,20), Date.civil(1975,03,20), Date.civil(1953,11,11)]
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should sort ascending on float" do
|
154
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("height ASC").all }.should use_cache(Person).on(:id)
|
155
|
+
@people.map(&:height).should == [1.69, 1.75, 1.75, 1.83]
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should sort descending on float" do
|
159
|
+
lambda{ @people = Person.where(:id => [1,2,3,4]).order("height DESC").all }.should use_cache(Person).on(:id)
|
160
|
+
@people.map(&:height).should == [1.83, 1.75, 1.75, 1.69]
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should sort on multiple fields (ASC + ASC)" do
|
164
|
+
lambda{ @people = Person.where(:id => [2,3,4,5]).order("height ASC, id ASC").all }.should use_cache(Person).on(:id)
|
165
|
+
@people.map(&:height).should == [1.69, 1.75, 1.75, 1.91]
|
166
|
+
@people.map(&:id).should == [4, 2, 3, 5]
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should sort on multiple fields (ASC + DESC)" do
|
170
|
+
lambda{ @people = Person.where(:id => [2,3,4,5]).order("height ASC, id DESC").all }.should use_cache(Person).on(:id)
|
171
|
+
@people.map(&:height).should == [1.69, 1.75, 1.75, 1.91]
|
172
|
+
@people.map(&:id).should == [4, 3, 2, 5]
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should sort on multiple fields (DESC + ASC)" do
|
176
|
+
lambda{ @people = Person.where(:id => [2,3,4,5]).order("height DESC, id ASC").all }.should use_cache(Person).on(:id)
|
177
|
+
@people.map(&:height).should == [1.91, 1.75, 1.75, 1.69]
|
178
|
+
@people.map(&:id).should == [5, 2, 3, 4]
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should sort on multiple fields (DESC + DESC)" do
|
182
|
+
lambda{ @people = Person.where(:id => [2,3,4,5]).order("height DESC, id DESC").all }.should use_cache(Person).on(:id)
|
183
|
+
@people.map(&:height).should == [1.91, 1.75, 1.75, 1.69]
|
184
|
+
@people.map(&:id).should == [5, 3, 2, 4]
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should use mysql style collation" do
|
188
|
+
ids = []
|
189
|
+
ids << Person.create!(:name => "ċedriĉ 3").id # latin other special
|
190
|
+
ids << Person.create!(:name => "a cedric").id # first in ascending order
|
191
|
+
ids << Person.create!(:name => "čedriĉ 4").id # latin another special
|
192
|
+
ids << Person.create!(:name => "ćedriĉ Last").id # latin special lowercase
|
193
|
+
ids << Person.create!(:name => "sedric 1").id # second to last latin in ascending order
|
194
|
+
ids << Person.create!(:name => "Cedric 2").id # ascii uppercase
|
195
|
+
ids << Person.create!(:name => "čedriĉ คฉ Almost last cedric").id # latin special, with non-latin
|
196
|
+
ids << Person.create!(:name => "Sedric 2").id # last latin in ascending order
|
197
|
+
ids << Person.create!(:name => "1 cedric").id # numbers before characters
|
198
|
+
ids << Person.create!(:name => "cedric 1").id # ascii lowercase
|
199
|
+
ids << Person.create!(:name => "คฉ Really last").id # non-latin characters last in ascending order
|
200
|
+
ids << Person.create!(:name => "čedriĉ ꜩ Last").id # latin special, with latin non-collateable
|
201
|
+
|
202
|
+
names_asc = ["1 cedric", "a cedric", "cedric 1", "Cedric 2", "ċedriĉ 3", "čedriĉ 4", "ćedriĉ Last", "čedriĉ คฉ Almost last cedric", "čedriĉ ꜩ Last", "sedric 1", "Sedric 2", "คฉ Really last"]
|
203
|
+
lambda{ @people = Person.where(:id => ids).order("name ASC").all }.should hit_cache(Person).on(:id).times(ids.size)
|
204
|
+
@people.map(&:name).should == names_asc
|
205
|
+
|
206
|
+
lambda{ @people = Person.where(:id => ids).order("name DESC").all }.should hit_cache(Person).on(:id).times(ids.size)
|
207
|
+
@people.map(&:name).should == names_asc.reverse
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should combine filter and sort" do
|
212
|
+
lambda{ @people = Person.where(:id => [1,2,3]).where(:height => 1.75).order("name DESC").all }.should use_cache(Person).on(:id)
|
213
|
+
@people.size.should == 2
|
214
|
+
@people.map(&:name).should == ["Cris", "Blue"]
|
215
|
+
|
216
|
+
lambda{ @people = Person.where(:id => [1,2,3]).where(:height => 1.75).order("name").all }.should hit_cache(Person).on(:id).times(3)
|
217
|
+
@people.map(&:name).should == ["Blue", "Cris"]
|
218
|
+
end
|
219
|
+
|
220
|
+
context "NotImplementedError" do
|
221
|
+
before(:each) do
|
222
|
+
@invalid_strategy = RecordCache::Strategy::Base.new(Object, nil, nil, "key")
|
223
|
+
end
|
224
|
+
|
225
|
+
it "should require record_change to be implemented" do
|
226
|
+
lambda { @invalid_strategy.record_change(Object.new, 1) }.should raise_error(NotImplementedError)
|
227
|
+
end
|
228
|
+
|
229
|
+
it "should require cacheable? to be implemented" do
|
230
|
+
lambda { @invalid_strategy.cacheable?(RecordCache::Query.new) }.should raise_error(NotImplementedError)
|
231
|
+
end
|
232
|
+
|
233
|
+
it "should require invalidate to be implemented" do
|
234
|
+
lambda { @invalid_strategy.invalidate(1) }.should raise_error(NotImplementedError)
|
235
|
+
end
|
236
|
+
|
237
|
+
it "should fetch_records to be implemented" do
|
238
|
+
lambda { @invalid_strategy.fetch(RecordCache::Query.new) }.should raise_error(NotImplementedError)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RecordCache::Strategy::IdCache do
|
4
|
+
|
5
|
+
it "should retrieve an Apple from the cache" do
|
6
|
+
lambda{ Apple.find(1) }.should miss_cache(Apple).on(:id).times(1)
|
7
|
+
lambda{ Apple.find(1) }.should hit_cache(Apple).on(:id).times(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should retrieve cloned records" do
|
11
|
+
@apple_1a = Apple.find(1)
|
12
|
+
@apple_1b = Apple.find(1)
|
13
|
+
@apple_1a.should == @apple_1b
|
14
|
+
@apple_1a.object_id.should_not == @apple_1b.object_id
|
15
|
+
end
|
16
|
+
|
17
|
+
context "logging" do
|
18
|
+
before(:each) do
|
19
|
+
Apple.find(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should write full hits to the debug log" do
|
23
|
+
mock(RecordCache::Base.logger).debug(/IdCache hit for ids 1|^(?!IdCache)/).times(any_times)
|
24
|
+
Apple.find(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should write full miss to the debug log" do
|
28
|
+
mock(RecordCache::Base.logger).debug(/IdCache miss for ids 2|^(?!IdCache)/).times(any_times)
|
29
|
+
Apple.find(2)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should write partial hits to the debug log" do
|
33
|
+
mock(RecordCache::Base.logger).debug(/IdCache partial hit for ids \[1, 2\]: missing \[2\]|^(?!IdCache)/).times(any_times)
|
34
|
+
Apple.where(:id => [1,2]).all
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "cacheable?" do
|
39
|
+
before(:each) do
|
40
|
+
# fill cache
|
41
|
+
@apple1 = Apple.find(1)
|
42
|
+
@apple2 = Apple.find(2)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should use the cache when a single id is requested" do
|
46
|
+
lambda{ Apple.where(:id => 1).all }.should hit_cache(Apple).on(:id).times(1)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should use the cache when a multiple ids are requested" do
|
50
|
+
lambda{ Apple.where(:id => [1, 2]).all }.should hit_cache(Apple).on(:id).times(2)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should use the cache when a single id is requested and the limit is 1" do
|
54
|
+
lambda{ Apple.where(:id => 1).limit(1).all }.should hit_cache(Apple).on(:id).times(1)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should not use the cache when a single id is requested and the limit is > 1" do
|
58
|
+
lambda{ Apple.where(:id => 1).limit(2).all }.should_not use_cache(Apple).on(:id)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not use the cache when multiple ids are requested and the limit is 1" do
|
62
|
+
lambda{ Apple.where(:id => [1, 2]).limit(1).all }.should_not use_cache(Apple).on(:id)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should use the cache when a single id is requested together with other where clauses" do
|
66
|
+
lambda{ Apple.where(:id => 1).where(:name => "Adams Apple x").all }.should hit_cache(Apple).on(:id).times(1)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should use the cache when a multiple ids are requested together with other where clauses" do
|
70
|
+
lambda{ Apple.where(:id => [1,2]).where(:name => "Adams Apple x").all }.should hit_cache(Apple).on(:id).times(2)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should use the cache when a single id is requested together with (simple) sort clauses" do
|
74
|
+
lambda{ Apple.where(:id => 1).order("name ASC").all }.should hit_cache(Apple).on(:id).times(1)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should use the cache when a multiple ids are requested together with (simple) sort clauses" do
|
78
|
+
lambda{ Apple.where(:id => [1,2]).order("name ASC").all }.should hit_cache(Apple).on(:id).times(2)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "record_change" do
|
83
|
+
before(:each) do
|
84
|
+
# fill cache
|
85
|
+
@apple1 = Apple.find(1)
|
86
|
+
@apple2 = Apple.find(2)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should invalidate destroyed records" do
|
90
|
+
lambda{ Apple.where(:id => 1).all }.should hit_cache(Apple).on(:id).times(1)
|
91
|
+
@apple1.destroy
|
92
|
+
lambda{ @apples = Apple.where(:id => 1).all }.should miss_cache(Apple).on(:id).times(1)
|
93
|
+
@apples.should == []
|
94
|
+
# try again, to make sure the "missing record" is not cached
|
95
|
+
lambda{ Apple.where(:id => 1).all }.should miss_cache(Apple).on(:id).times(1)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should add updated records directly to the cache" do
|
99
|
+
@apple1.name = "Applejuice"
|
100
|
+
@apple1.save!
|
101
|
+
lambda{ @apple = Apple.find(1) }.should hit_cache(Apple).on(:id).times(1)
|
102
|
+
@apple.name.should == "Applejuice"
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should add created records directly to the cache" do
|
106
|
+
@new_apple = Apple.create!(:name => "Fresh Apple", :store_id => 1)
|
107
|
+
lambda{ @apple = Apple.find(@new_apple.id) }.should hit_cache(Apple).on(:id).times(1)
|
108
|
+
@apple.name.should == "Fresh Apple"
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should add updated records to the cache, also when multiple ids are queried" do
|
112
|
+
@apple1.name = "Applejuice"
|
113
|
+
@apple1.save!
|
114
|
+
lambda{ @apples = Apple.where(:id => [1, 2]).order('id ASC').all }.should hit_cache(Apple).on(:id).times(2)
|
115
|
+
@apples.map(&:name).should == ["Applejuice", "Adams Apple 2"]
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
context "invalidate" do
|
121
|
+
before(:each) do
|
122
|
+
@apple1 = Apple.find(1)
|
123
|
+
@apple2 = Apple.find(2)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should invalidate single records" do
|
127
|
+
Apple.record_cache[:id].invalidate(1)
|
128
|
+
lambda{ Apple.find(1) }.should miss_cache(Apple).on(:id).times(1)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should only miss the cache for the invalidated record when multiple ids are queried" do
|
132
|
+
# miss on 1
|
133
|
+
Apple.record_cache[:id].invalidate(1)
|
134
|
+
lambda{ Apple.where(:id => [1, 2]).all }.should miss_cache(Apple).on(:id).times(1)
|
135
|
+
# hit on 2
|
136
|
+
Apple.record_cache[:id].invalidate(1)
|
137
|
+
lambda{ Apple.where(:id => [1, 2]).all }.should hit_cache(Apple).on(:id).times(1)
|
138
|
+
# nothing invalidated, both hit
|
139
|
+
lambda{ Apple.where(:id => [1, 2]).all }.should hit_cache(Apple).on(:id).times(2)
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should invalidate records when using update_all" do
|
143
|
+
Apple.where(:id => [3,4,5]).all # fill id cache on all Adam Store apples
|
144
|
+
lambda{ @apples = Apple.where(:id => [1, 2, 3, 4, 5]).order('id ASC').all }.should hit_cache(Apple).on(:id).times(5)
|
145
|
+
@apples.map(&:name).should == ["Adams Apple 1", "Adams Apple 2", "Adams Apple 3", "Adams Apple 4", "Adams Apple 5"]
|
146
|
+
# update 3 of the 5 apples in the Adam Store
|
147
|
+
Apple.where(:id => [1,2,3]).update_all(:name => "Uniform Apple")
|
148
|
+
lambda{ @apples = Apple.where(:id => [1, 2, 3, 4, 5]).order('id ASC').all }.should hit_cache(Apple).on(:id).times(2)
|
149
|
+
@apples.map(&:name).should == ["Uniform Apple", "Uniform Apple", "Uniform Apple", "Adams Apple 4", "Adams Apple 5"]
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should invalidate reflection indexes when a has_many relation is updated" do
|
153
|
+
# assign different apples to store 2
|
154
|
+
lambda{ Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:id).times(2)
|
155
|
+
store2_apple_ids = Apple.where(:store_id => 2).map(&:id)
|
156
|
+
store1 = Store.find(1)
|
157
|
+
store1.apple_ids = store2_apple_ids
|
158
|
+
store1.save!
|
159
|
+
# the apples that used to belong to store 2 are now in store 1 (incremental update)
|
160
|
+
lambda{ @apple1 = Apple.find(store2_apple_ids.first) }.should hit_cache(Apple).on(:id).times(1)
|
161
|
+
@apple1.store_id.should == 1
|
162
|
+
# the apples that used to belong to store 1 are now homeless (cache invalidated)
|
163
|
+
lambda{ @homeless_apple = Apple.find(1) }.should miss_cache(Apple).on(:id).times(1)
|
164
|
+
@homeless_apple.store_id.should == nil
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RecordCache::Strategy::IndexCache do
|
4
|
+
|
5
|
+
context "initialize" do
|
6
|
+
it "should only accept index cache on DB columns" do
|
7
|
+
lambda { Apple.send(:cache_records, :index => :unknown_column) }.should raise_error("No column found for index 'unknown_column' on Apple.")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should only accept index cache on integer columns" do
|
11
|
+
lambda { Apple.send(:cache_records, :index => :name) }.should raise_error("Incorrect type (expected integer, found string) for index 'name' on Apple.")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should use the id cache to retrieve the actual records" do
|
16
|
+
lambda { @apples = Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
|
17
|
+
lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
|
18
|
+
lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:id).times(@apples.size)
|
19
|
+
end
|
20
|
+
|
21
|
+
context "logging" do
|
22
|
+
before(:each) do
|
23
|
+
Apple.where(:store_id => 1).all
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should write hit to the debug log" do
|
27
|
+
mock(RecordCache::Base.logger).debug(/IndexCache hit for rc\/apl\/store_id=1v\d+: found 5 ids|^(?!IndexCache)/).times(any_times)
|
28
|
+
Apple.where(:store_id => 1).all
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should write miss to the debug log" do
|
32
|
+
mock(RecordCache::Base.logger).debug(/IndexCache miss for rc\/apl\/store_id=2v\d+: found no ids|^(?!IndexCache)/).times(any_times)
|
33
|
+
Apple.where(:store_id => 2).all
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "cacheable?" do
|
38
|
+
before(:each) do
|
39
|
+
@store1_apples = Apple.where(:store_id => 1).all
|
40
|
+
@store2_apples = Apple.where(:store_id => 2).all
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should hit the cache for a single index id" do
|
44
|
+
lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should hit the cache for a single index id with other where clauses" do
|
48
|
+
lambda { Apple.where(:store_id => 1).where(:name => "applegate").all }.should hit_cache(Apple).on(:store_id).times(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should hit the cache for a single index id with (simple) sort clauses" do
|
52
|
+
lambda { Apple.where(:store_id => 1).order("name ASC").all }.should hit_cache(Apple).on(:store_id).times(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should not hit the cache for a single index id with limit" do
|
56
|
+
lambda { Apple.where(:store_id => 1).limit(1).all }.should_not hit_cache(Apple).on(:store_id)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should not hit the cache when an :id where clause is defined" do
|
60
|
+
# this query should make use of the :id cache, which is faster
|
61
|
+
lambda { Apple.where(:store_id => 1).where(:id => 1).all }.should_not hit_cache(Apple).on(:store_id)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "record_change" do
|
66
|
+
before(:each) do
|
67
|
+
@store1_apples = Apple.where(:store_id => 1).order('id ASC').all
|
68
|
+
@store2_apples = Apple.where(:store_id => 2).order('id ASC').all
|
69
|
+
end
|
70
|
+
|
71
|
+
[false, true].each do |fresh|
|
72
|
+
it "should #{fresh ? 'update' : 'invalidate'} the index when a record in the index is destroyed and the current index is #{fresh ? '' : 'not '}fresh" do
|
73
|
+
# make sure the index is no longer fresh
|
74
|
+
Apple.record_cache.invalidate(:store_id, 1) unless fresh
|
75
|
+
# destroy an apple
|
76
|
+
@destroyed = @store1_apples.last
|
77
|
+
@destroyed.destroy
|
78
|
+
# check the cache hit/miss on the index that contained that apple
|
79
|
+
if fresh
|
80
|
+
lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
81
|
+
else
|
82
|
+
lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
|
83
|
+
end
|
84
|
+
@apples.size.should == @store1_apples.size - 1
|
85
|
+
@apples.map(&:id).should == @store1_apples.map(&:id) - [@destroyed.id]
|
86
|
+
# and the index should be cached again
|
87
|
+
lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should #{fresh ? 'update' : 'invalidate'} the index when a record in the index is created and the current index is #{fresh ? '' : 'not '}fresh" do
|
91
|
+
# make sure the index is no longer fresh
|
92
|
+
Apple.record_cache.invalidate(:store_id, 1) unless fresh
|
93
|
+
# create an apple
|
94
|
+
@new_apple_in_store1 = Apple.create!(:name => "Fresh Apple", :store_id => 1)
|
95
|
+
# check the cache hit/miss on the index that contains that apple
|
96
|
+
if fresh
|
97
|
+
lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
98
|
+
else
|
99
|
+
lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
|
100
|
+
end
|
101
|
+
@apples.size.should == @store1_apples.size + 1
|
102
|
+
@apples.map(&:id).should == @store1_apples.map(&:id) + [@new_apple_in_store1.id]
|
103
|
+
# and the index should be cached again
|
104
|
+
lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should #{fresh ? 'update' : 'invalidate'} two indexes when the indexed value is updated and the current index is #{fresh ? '' : 'not '}fresh" do
|
108
|
+
# make sure both indexes are no longer fresh
|
109
|
+
Apple.record_cache.invalidate(:store_id, 1) unless fresh
|
110
|
+
Apple.record_cache.invalidate(:store_id, 2) unless fresh
|
111
|
+
# move one apple from store 1 to store 2
|
112
|
+
@apple_moved_from_store1_to_store2 = @store1_apples.last
|
113
|
+
@apple_moved_from_store1_to_store2.store_id = 2
|
114
|
+
@apple_moved_from_store1_to_store2.save!
|
115
|
+
# check the cache hit/miss on the indexes that contained/contains that apple
|
116
|
+
if fresh
|
117
|
+
lambda { @apples1 = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
118
|
+
lambda { @apples2 = Apple.where(:store_id => 2).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
119
|
+
else
|
120
|
+
lambda { @apples1 = Apple.where(:store_id => 1).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
|
121
|
+
lambda { @apples2 = Apple.where(:store_id => 2).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
|
122
|
+
end
|
123
|
+
@apples1.size.should == @store1_apples.size - 1
|
124
|
+
@apples2.size.should == @store2_apples.size + 1
|
125
|
+
@apples1.map(&:id).should == @store1_apples.map(&:id) - [@apple_moved_from_store1_to_store2.id]
|
126
|
+
@apples2.map(&:id).should == [@apple_moved_from_store1_to_store2.id] + @store2_apples.map(&:id)
|
127
|
+
# and the index should be cached again
|
128
|
+
lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
|
129
|
+
lambda { Apple.where(:store_id => 2).all }.should hit_cache(Apple).on(:store_id).times(1)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should #{fresh ? 'update' : 'invalidate'} multiple indexes when several values on different indexed attributes are updated at once and one of the indexes is #{fresh ? '' : 'not '}fresh" do
|
133
|
+
# find the apples for person 1 and 5 (Chase)
|
134
|
+
@person4_apples = Apple.where(:person_id => 4).all # Fry's Apples
|
135
|
+
@person5_apples = Apple.where(:person_id => 5).all # Chases' Apples
|
136
|
+
# make sure person indexes are no longer fresh
|
137
|
+
Apple.record_cache.invalidate(:person_id, 4) unless fresh
|
138
|
+
Apple.record_cache.invalidate(:person_id, 5) unless fresh
|
139
|
+
# move one apple from store 1 to store 2
|
140
|
+
@apple_moved_from_s1to2_p5to4 = @store1_apples.last # the last apple belongs to person Chase (id 5)
|
141
|
+
@apple_moved_from_s1to2_p5to4.store_id = 2
|
142
|
+
@apple_moved_from_s1to2_p5to4.person_id = 4
|
143
|
+
@apple_moved_from_s1to2_p5to4.save!
|
144
|
+
# check the cache hit/miss on the indexes that contained/contains that apple
|
145
|
+
lambda { @apples_s1 = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
146
|
+
lambda { @apples_s2 = Apple.where(:store_id => 2).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
147
|
+
if fresh
|
148
|
+
lambda { @apples_p1 = Apple.where(:person_id => 4).order('id ASC').all }.should hit_cache(Apple).on(:person_id).times(1)
|
149
|
+
lambda { @apples_p2 = Apple.where(:person_id => 5).order('id ASC').all }.should hit_cache(Apple).on(:person_id).times(1)
|
150
|
+
else
|
151
|
+
lambda { @apples_p1 = Apple.where(:person_id => 4).order('id ASC').all }.should miss_cache(Apple).on(:person_id).times(1)
|
152
|
+
lambda { @apples_p2 = Apple.where(:person_id => 5).order('id ASC').all }.should miss_cache(Apple).on(:person_id).times(1)
|
153
|
+
end
|
154
|
+
@apples_s1.size.should == @store1_apples.size - 1
|
155
|
+
@apples_s2.size.should == @store2_apples.size + 1
|
156
|
+
@apples_p1.size.should == @person4_apples.size + 1
|
157
|
+
@apples_p2.size.should == @person5_apples.size - 1
|
158
|
+
@apples_s1.map(&:id).should == @store1_apples.map(&:id) - [@apple_moved_from_s1to2_p5to4.id]
|
159
|
+
@apples_s2.map(&:id).should == [@apple_moved_from_s1to2_p5to4.id] + @store2_apples.map(&:id)
|
160
|
+
@apples_p1.map(&:id).should == ([@apple_moved_from_s1to2_p5to4.id] + @person4_apples.map(&:id)).sort
|
161
|
+
@apples_p2.map(&:id).should == (@person5_apples.map(&:id) - [@apple_moved_from_s1to2_p5to4.id]).sort
|
162
|
+
# and the index should be cached again
|
163
|
+
lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
|
164
|
+
lambda { Apple.where(:store_id => 2).all }.should hit_cache(Apple).on(:store_id).times(1)
|
165
|
+
lambda { Apple.where(:person_id => 4).all }.should hit_cache(Apple).on(:person_id).times(1)
|
166
|
+
lambda { Apple.where(:person_id => 5).all }.should hit_cache(Apple).on(:person_id).times(1)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should leave the index alone when a record outside the index is destroyed" do
|
171
|
+
# destroy an apple of store 2
|
172
|
+
@store2_apples.first.destroy
|
173
|
+
# index of apples of store 1 are not affected
|
174
|
+
lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should leave the index alone when a record outside the index is created" do
|
178
|
+
# create an apple for store 2
|
179
|
+
Apple.create!(:name => "Fresh Apple", :store_id => 2)
|
180
|
+
# index of apples of store 1 are not affected
|
181
|
+
lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
context "invalidate" do
|
186
|
+
before(:each) do
|
187
|
+
@store1_apples = Apple.where(:store_id => 1).all
|
188
|
+
@store2_apples = Apple.where(:store_id => 2).all
|
189
|
+
end
|
190
|
+
|
191
|
+
it "should invalidate single index" do
|
192
|
+
Apple.record_cache[:store_id].invalidate(1)
|
193
|
+
lambda{ Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should invalidate indexes when using update_all" do
|
197
|
+
pending "is there a performant way to invalidate index caches within update_all? only the new value is available, so we should query the old values..." do
|
198
|
+
# update 2 apples for index values store 1 and store 2
|
199
|
+
Apple.where(:id => [@store1_apples.first.id, @store2_apples.first.id]).update_all(:store_id => 3)
|
200
|
+
lambda{ @apples_1 = Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
|
201
|
+
lambda{ @apples_2 = Apple.where(:store_id => 2).all }.should miss_cache(Apple).on(:store_id).times(1)
|
202
|
+
@apples_1.map(&:id).sort.should == @store1_apples[1..-1].sort
|
203
|
+
@apples_2.map(&:id).sort.should == @store2_apples[1..-1].sort
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should invalidate reflection indexes when a has_many relation is updated" do
|
208
|
+
# assign different apples to store 2
|
209
|
+
lambda{ Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
|
210
|
+
store2_apple_ids = @store2_apples.map(&:id).sort
|
211
|
+
store1 = Store.find(1)
|
212
|
+
store1.apple_ids = store2_apple_ids
|
213
|
+
store1.save!
|
214
|
+
# apples in Store 1 should be all (only) the apples that were in Store 2 (cache invalidated)
|
215
|
+
lambda{ @apples_1 = Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
|
216
|
+
@apples_1.map(&:id).sort.should == store2_apple_ids
|
217
|
+
# there are no apples in Store 2 anymore (incremental cache update, as each apples in store 2 was saved separately)
|
218
|
+
lambda{ @apples_2 = Apple.where(:store_id => 2).all }.should hit_cache(Apple).on(:store_id).times(1)
|
219
|
+
@apples_2.should == []
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|