record-cache 0.1.0
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/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
|