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.
Files changed (39) hide show
  1. data/lib/record-cache.rb +1 -0
  2. data/lib/record_cache/active_record.rb +318 -0
  3. data/lib/record_cache/base.rb +136 -0
  4. data/lib/record_cache/dispatcher.rb +90 -0
  5. data/lib/record_cache/multi_read.rb +51 -0
  6. data/lib/record_cache/query.rb +85 -0
  7. data/lib/record_cache/statistics.rb +82 -0
  8. data/lib/record_cache/strategy/base.rb +154 -0
  9. data/lib/record_cache/strategy/id_cache.rb +93 -0
  10. data/lib/record_cache/strategy/index_cache.rb +122 -0
  11. data/lib/record_cache/strategy/request_cache.rb +49 -0
  12. data/lib/record_cache/test/resettable_version_store.rb +49 -0
  13. data/lib/record_cache/version.rb +5 -0
  14. data/lib/record_cache/version_store.rb +54 -0
  15. data/lib/record_cache.rb +11 -0
  16. data/spec/db/database.yml +6 -0
  17. data/spec/db/schema.rb +42 -0
  18. data/spec/db/seeds.rb +40 -0
  19. data/spec/initializers/record_cache.rb +14 -0
  20. data/spec/lib/dispatcher_spec.rb +86 -0
  21. data/spec/lib/multi_read_spec.rb +51 -0
  22. data/spec/lib/query_spec.rb +148 -0
  23. data/spec/lib/statistics_spec.rb +140 -0
  24. data/spec/lib/strategy/base_spec.rb +241 -0
  25. data/spec/lib/strategy/id_cache_spec.rb +168 -0
  26. data/spec/lib/strategy/index_cache_spec.rb +223 -0
  27. data/spec/lib/strategy/request_cache_spec.rb +85 -0
  28. data/spec/lib/version_store_spec.rb +104 -0
  29. data/spec/models/apple.rb +8 -0
  30. data/spec/models/banana.rb +8 -0
  31. data/spec/models/pear.rb +6 -0
  32. data/spec/models/person.rb +11 -0
  33. data/spec/models/store.rb +13 -0
  34. data/spec/spec_helper.rb +44 -0
  35. data/spec/support/after_commit.rb +71 -0
  36. data/spec/support/matchers/hit_cache_matcher.rb +53 -0
  37. data/spec/support/matchers/miss_cache_matcher.rb +53 -0
  38. data/spec/support/matchers/use_cache_matcher.rb +53 -0
  39. 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