record-cache 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +15 -0
  2. data/lib/record_cache.rb +2 -1
  3. data/lib/record_cache/base.rb +63 -22
  4. data/lib/record_cache/datastore/active_record.rb +5 -3
  5. data/lib/record_cache/datastore/active_record_30.rb +95 -38
  6. data/lib/record_cache/datastore/active_record_31.rb +157 -54
  7. data/lib/record_cache/datastore/active_record_32.rb +444 -0
  8. data/lib/record_cache/dispatcher.rb +47 -47
  9. data/lib/record_cache/multi_read.rb +14 -1
  10. data/lib/record_cache/query.rb +36 -25
  11. data/lib/record_cache/statistics.rb +5 -5
  12. data/lib/record_cache/strategy/base.rb +49 -19
  13. data/lib/record_cache/strategy/full_table_cache.rb +81 -0
  14. data/lib/record_cache/strategy/index_cache.rb +38 -36
  15. data/lib/record_cache/strategy/unique_index_cache.rb +130 -0
  16. data/lib/record_cache/strategy/util.rb +12 -12
  17. data/lib/record_cache/test/resettable_version_store.rb +2 -9
  18. data/lib/record_cache/version.rb +1 -1
  19. data/lib/record_cache/version_store.rb +23 -16
  20. data/spec/db/schema.rb +12 -0
  21. data/spec/db/seeds.rb +10 -0
  22. data/spec/lib/active_record/visitor_spec.rb +22 -0
  23. data/spec/lib/base_spec.rb +21 -0
  24. data/spec/lib/dispatcher_spec.rb +24 -46
  25. data/spec/lib/multi_read_spec.rb +6 -6
  26. data/spec/lib/query_spec.rb +43 -43
  27. data/spec/lib/statistics_spec.rb +28 -28
  28. data/spec/lib/strategy/base_spec.rb +98 -87
  29. data/spec/lib/strategy/full_table_cache_spec.rb +68 -0
  30. data/spec/lib/strategy/index_cache_spec.rb +112 -69
  31. data/spec/lib/strategy/query_cache_spec.rb +83 -0
  32. data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +317 -0
  33. data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +168 -0
  34. data/spec/lib/strategy/util_spec.rb +67 -49
  35. data/spec/lib/version_store_spec.rb +22 -41
  36. data/spec/models/address.rb +9 -0
  37. data/spec/models/apple.rb +1 -1
  38. data/spec/models/banana.rb +21 -2
  39. data/spec/models/language.rb +5 -0
  40. data/spec/models/person.rb +1 -1
  41. data/spec/models/store.rb +2 -1
  42. data/spec/spec_helper.rb +7 -4
  43. data/spec/support/after_commit.rb +2 -0
  44. data/spec/support/matchers/hit_cache_matcher.rb +10 -6
  45. data/spec/support/matchers/log.rb +45 -0
  46. data/spec/support/matchers/miss_cache_matcher.rb +10 -6
  47. data/spec/support/matchers/use_cache_matcher.rb +10 -6
  48. metadata +156 -161
  49. data/lib/record_cache/strategy/id_cache.rb +0 -93
  50. data/lib/record_cache/strategy/request_cache.rb +0 -49
  51. data/spec/lib/strategy/id_cache_spec.rb +0 -168
  52. data/spec/lib/strategy/request_cache_spec.rb +0 -85
@@ -1,93 +0,0 @@
1
- module RecordCache
2
- module Strategy
3
- class IdCache < Base
4
-
5
- # Can the cache retrieve the records based on this query?
6
- def cacheable?(query)
7
- ids = query.where_ids(:id)
8
- ids && (query.limit.nil? || (query.limit == 1 && ids.size == 1))
9
- end
10
-
11
- # Update the version store and the record store
12
- def record_change(record, action)
13
- key = cache_key(record.id)
14
- if action == :destroy
15
- version_store.delete(key)
16
- else
17
- # update the version store and add the record to the cache
18
- new_version = version_store.increment(key)
19
- record_store.write(versioned_key(key, new_version), Util.serialize(record))
20
- end
21
- end
22
-
23
- # Handle invalidation call
24
- def invalidate(id)
25
- version_store.delete(cache_key(id))
26
- end
27
-
28
- protected
29
-
30
- # retrieve the record(s) with the given id(s) as an array
31
- def fetch_records(query)
32
- ids = query.where_ids(:id)
33
- query.wheres.delete(:id) # make sure CacheCase.filter! does not see this where anymore
34
- id_to_key_map = ids.inject({}){|h,id| h[id] = cache_key(id); h }
35
- # retrieve the current version of the records
36
- current_versions = version_store.current_multi(id_to_key_map)
37
- # get the keys for the records for which a current version was found
38
- id_to_version_key_map = Hash[id_to_key_map.map{ |id, key| current_versions[id] ? [id, versioned_key(key, current_versions[id])] : nil }]
39
- # retrieve the records from the cache
40
- records = id_to_version_key_map.size > 0 ? from_cache(id_to_version_key_map) : []
41
- # query the records with missing ids
42
- id_to_key_map.except!(*records.map(&:id))
43
- # logging (only in debug mode!) and statistics
44
- log_id_cache_hit(ids, id_to_key_map.keys) if RecordCache::Base.logger.debug?
45
- statistics.add(ids.size, records.size) if statistics.active?
46
- # retrieve records from DB in case there are some missing ids
47
- records += from_db(id_to_key_map, id_to_version_key_map) if id_to_key_map.size > 0
48
- # return the array
49
- records
50
- end
51
-
52
- private
53
-
54
- # ---------------------------- Querying ------------------------------------
55
-
56
- # retrieve the records from the cache with the given keys
57
- def from_cache(id_to_versioned_key_map)
58
- records = record_store.read_multi(*(id_to_versioned_key_map.values)).values.compact
59
- records.map{ |record| Util.deserialize(record) }
60
- end
61
-
62
- # retrieve the records with the given ids from the database
63
- def from_db(id_to_key_map, id_to_version_key_map)
64
- RecordCache::Base.without_record_cache do
65
- # retrieve the records from the database
66
- records = @base.where(:id => id_to_key_map.keys).to_a
67
- records.each do |record|
68
- versioned_key = id_to_version_key_map[record.id]
69
- unless versioned_key
70
- # renew the key in the version store in case it was missing
71
- key = id_to_key_map[record.id]
72
- versioned_key = versioned_key(key, version_store.renew(key))
73
- end
74
- # store the record based on the versioned key
75
- record_store.write(versioned_key, Util.serialize(record))
76
- end
77
- records
78
- end
79
- end
80
-
81
- # ------------------------- Utility methods ----------------------------
82
-
83
- # log cache hit/miss to debug log
84
- def log_id_cache_hit(ids, missing_ids)
85
- hit = missing_ids.empty? ? "hit" : ids.size == missing_ids.size ? "miss" : "partial hit"
86
- missing = missing_ids.empty? || ids.size == missing_ids.size ? "" : ": missing #{missing_ids.inspect}"
87
- msg = "IdCache #{hit} for ids #{ids.size == 1 ? ids.first : ids.inspect}#{missing}"
88
- RecordCache::Base.logger.debug(msg)
89
- end
90
-
91
- end
92
- end
93
- end
@@ -1,49 +0,0 @@
1
- # Remembers the queries performed during a single Request.
2
- # If the same query is requested again the result is provided straight from local memory.
3
- #
4
- # Records are invalidated per model-klass, when any record is created, updated or destroyed.
5
- module RecordCache
6
- module Strategy
7
-
8
- class RequestCache < Base
9
- @@request_store = {}
10
-
11
- # call before each request: in application_controller.rb
12
- # before_filter { |c| RecordCache::Strategy::RequestCache.clear }
13
- def self.clear
14
- @@request_store.clear
15
- end
16
-
17
- # Handle record change
18
- def record_change(record, action)
19
- @@request_store.delete(@base.name)
20
- end
21
-
22
- # Handle invalidation call
23
- def invalidate(value)
24
- @@request_store.delete(@base.name)
25
- end
26
-
27
- # return the records from the request cache, execute block in case
28
- # this is the first time this query is performed during this request
29
- def fetch(query, &block)
30
- klass_store = (@@request_store[@base.name] ||= {})
31
- key = query.cache_key
32
- # logging (only in debug mode!) and statistics
33
- log_cache_hit(key, klass_store.key?(key)) if RecordCache::Base.logger.debug?
34
- statistics.add(1, klass_store.key?(key) ? 1 : 0) if statistics.active?
35
- klass_store[key] ||= yield
36
- end
37
-
38
- private
39
-
40
- # ------------------------- Utility methods ----------------------------
41
-
42
- # log cache hit/miss to debug log
43
- def log_cache_hit(key, hit)
44
- RecordCache::Base.logger.debug("RequestCache #{hit ? 'hit' : 'miss'} for #{key}")
45
- end
46
-
47
- end
48
- end
49
- end
@@ -1,168 +0,0 @@
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
@@ -1,85 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe RecordCache::Strategy::RequestCache do
4
-
5
- it "should retrieve a record from the Request Cache" do
6
- lambda{ Store.find(1) }.should miss_cache(Store)
7
- lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
8
- end
9
-
10
- it "should retrieve the same record when the same query is used" do
11
- @store_1 = Store.find(1)
12
- @store_2 = Store.find(1)
13
- @store_1.should == @store_2
14
- @store_1.object_id.should == @store_2.object_id
15
- end
16
-
17
- context "logging" do
18
- before(:each) do
19
- Store.find(1)
20
- end
21
-
22
- it "should write hit to the debug log" do
23
- mock(RecordCache::Base.logger).debug(/RequestCache hit for id=1\.L1|^(?!RequestCache)/).times(any_times)
24
- Store.find(1)
25
- end
26
-
27
- it "should write miss to the debug log" do
28
- mock(RecordCache::Base.logger).debug(/^RequestCache miss for id=2.L1|^(?!RequestCache)/).times(any_times)
29
- Store.find(2)
30
- end
31
- end
32
-
33
- context "record_change" do
34
- before(:each) do
35
- # cache query in request cache
36
- @store1 = Store.find(1)
37
- @store2 = Store.find(2)
38
- end
39
-
40
- it "should remove all records from the cache for a specific model when one record is destroyed" do
41
- lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
42
- lambda{ Store.find(2) }.should hit_cache(Store).on(:request_cache).times(1)
43
- @store1.destroy
44
- lambda{ Store.find(2) }.should miss_cache(Store).on(:request_cache).times(1)
45
- end
46
-
47
- it "should remove all records from the cache for a specific model when one record is updated" do
48
- lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
49
- lambda{ Store.find(2) }.should hit_cache(Store).on(:request_cache).times(1)
50
- @store1.name = "Store E"
51
- @store1.save!
52
- lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
53
- lambda{ Store.find(2) }.should miss_cache(Store).on(:request_cache).times(1)
54
- end
55
-
56
- it "should remove all records from the cache for a specific model when one record is created" do
57
- lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
58
- lambda{ Store.find(2) }.should hit_cache(Store).on(:request_cache).times(1)
59
- Store.create!(:name => "New Apple Store")
60
- lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
61
- lambda{ Store.find(2) }.should miss_cache(Store).on(:request_cache).times(1)
62
- end
63
-
64
- end
65
-
66
- context "invalidate" do
67
- before(:each) do
68
- # cache query in request cache
69
- @store1 = Store.find(1)
70
- @store2 = Store.find(2)
71
- end
72
-
73
- it "should remove all records from the cache when clear is explicitly called" do
74
- lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
75
- RecordCache::Strategy::RequestCache.clear
76
- lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
77
- end
78
-
79
- it "should remove all records from the cache when invalidate is called" do
80
- lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
81
- Store.record_cache.invalidate(:request_cache, @store2)
82
- lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
83
- end
84
- end
85
- end