record-cache 0.1.2 → 0.1.3

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 (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