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
data/spec/db/seeds.rb ADDED
@@ -0,0 +1,40 @@
1
+ ActiveRecord::Schema.define :version => 1 do
2
+
3
+ # Make sure that at the beginning of the tests, NOTHING is known to Record Cache
4
+ RecordCache::Base.disable!
5
+
6
+ @adam = Person.create!(:name => "Adam", :birthday => Date.civil(1975,03,20), :height => 1.83)
7
+ @blue = Person.create!(:name => "Blue", :birthday => Date.civil(1953,11,11), :height => 1.75)
8
+ @cris = Person.create!(:name => "Cris", :birthday => Date.civil(1975,03,20), :height => 1.75)
9
+
10
+ @adam_apples = Store.create!(:name => "Adams Apple Store", :owner => @adam)
11
+ @blue_fruits = Store.create!(:name => "Blue Fruits", :owner => @blue)
12
+ @cris_bananas = Store.create!(:name => "Chris Bananas", :owner => @cris)
13
+
14
+ @fry = Person.create!(:name => "Fry", :birthday => Date.civil(1985,01,20), :height => 1.69)
15
+ @chase = Person.create!(:name => "Chase", :birthday => Date.civil(1970,07,03), :height => 1.91)
16
+ @penny = Person.create!(:name => "Penny", :birthday => Date.civil(1958,04,16), :height => 1.61)
17
+
18
+ Apple.create!(:name => "Adams Apple 1", :store => @adam_apples)
19
+ Apple.create!(:name => "Adams Apple 2", :store => @adam_apples)
20
+ Apple.create!(:name => "Adams Apple 3", :store => @adam_apples, :person => @fry)
21
+ Apple.create!(:name => "Adams Apple 4", :store => @adam_apples, :person => @fry)
22
+ Apple.create!(:name => "Adams Apple 5", :store => @adam_apples, :person => @chase)
23
+ Apple.create!(:name => "Blue Apple 1", :store => @blue_fruits, :person => @fry)
24
+ Apple.create!(:name => "Blue Apple 2", :store => @blue_fruits, :person => @fry)
25
+ Apple.create!(:name => "Blue Apple 3", :store => @blue_fruits, :person => @chase)
26
+ Apple.create!(:name => "Blue Apple 4", :store => @blue_fruits, :person => @chase)
27
+
28
+ Banana.create!(:name => "Blue Banana 1", :store => @blue_fruits, :person => @fry)
29
+ Banana.create!(:name => "Blue Banana 2", :store => @blue_fruits, :person => @chase)
30
+ Banana.create!(:name => "Blue Banana 3", :store => @blue_fruits, :person => @chase)
31
+ Banana.create!(:name => "Cris Banana 1", :store => @cris_bananas, :person => @fry)
32
+ Banana.create!(:name => "Cris Banana 2", :store => @cris_bananas, :person => @chase)
33
+
34
+ Pear.create!(:name => "Blue Pear 1", :store => @blue_fruits)
35
+ Pear.create!(:name => "Blue Pear 2", :store => @blue_fruits, :person => @fry)
36
+ Pear.create!(:name => "Blue Pear 3", :store => @blue_fruits, :person => @chase)
37
+ Pear.create!(:name => "Blue Pear 4", :store => @blue_fruits, :person => @chase)
38
+
39
+ RecordCache::Base.enable
40
+ end
@@ -0,0 +1,14 @@
1
+ # --- Version Store
2
+ # All Workers that use the Record Cache should point to the same Version Store
3
+ # E.g. a MemCached cluster or a Redis Store (defaults to Rails.cache)
4
+ RecordCache::Base.version_store = ActiveSupport::Cache.lookup_store(:memory_store)
5
+
6
+ # --- Record Stores
7
+ # Register Cache Stores for the Records themselves
8
+ # Note: A different Cache Store could be used per Model, but in most configurations the following 2 stores will suffice:
9
+
10
+ # The :local store is used to keep records in Worker memory
11
+ RecordCache::Base.register_store(:local, ActiveSupport::Cache.lookup_store(:memory_store))
12
+
13
+ # The :shared store is used to share Records between multiple Workers
14
+ RecordCache::Base.register_store(:shared, ActiveSupport::Cache.lookup_store(:memory_store))
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe RecordCache::Dispatcher do
4
+ before(:each) do
5
+ @apple_dispatcher = Apple.record_cache
6
+ end
7
+
8
+ it "should raise an error when the same index is added twice" do
9
+ lambda { @apple_dispatcher.register(:store_id, RecordCache::Strategy::IdCache, nil, {}) }.should raise_error("Multiple record cache definitions found for 'store_id' on Apple")
10
+ end
11
+
12
+ it "should return the Cache for the requested strategy" do
13
+ @apple_dispatcher[:id].class.should == RecordCache::Strategy::IdCache
14
+ @apple_dispatcher[:store_id].class.should == RecordCache::Strategy::IndexCache
15
+ end
16
+
17
+ it "should return nil for unknown requested strategies" do
18
+ @apple_dispatcher[:unknown].should == nil
19
+ end
20
+
21
+ it "should return cacheable? true if there is a cacheable strategy that accepts the query" do
22
+ query = RecordCache::Query.new
23
+ mock(@apple_dispatcher).first_cacheable_strategy(query) { Object.new }
24
+ @apple_dispatcher.cacheable?(query).should == true
25
+ end
26
+
27
+ context "fetch" do
28
+ it "should delegate fetch to the Request Cache if present" do
29
+ query = RecordCache::Query.new
30
+ mock(@apple_dispatcher[:request_cache]).fetch(query)
31
+ @apple_dispatcher.fetch(query)
32
+ end
33
+
34
+ it "should delegate fetch to the first cacheable strategy if Request Cache is not present" do
35
+ query = RecordCache::Query.new
36
+ banana_dispatcher = Banana.record_cache
37
+ banana_dispatcher[:request_cache].should == nil
38
+ mock(banana_dispatcher).first_cacheable_strategy(query) { mock(Object.new).fetch(query) }
39
+ banana_dispatcher.fetch(query)
40
+ end
41
+ end
42
+
43
+ context "record_change" do
44
+ it "should dispatch record_change to all strategies" do
45
+ apple = Apple.first
46
+ [:id, :store_id, :person_id].each do |strategy|
47
+ mock(@apple_dispatcher[strategy]).record_change(apple, :create)
48
+ end
49
+ @apple_dispatcher.record_change(apple, :create)
50
+ end
51
+
52
+ it "should not dispatch record_change for updates without changes" do
53
+ apple = Apple.first
54
+ [:request_cache, :id, :store_id, :person_id].each do |strategy|
55
+ mock(@apple_dispatcher[strategy]).record_change(anything, anything).times(0)
56
+ end
57
+ @apple_dispatcher.record_change(apple, :update)
58
+ end
59
+ end
60
+
61
+ context "invalidate" do
62
+ it "should default to the :id strategy" do
63
+ mock(@apple_dispatcher[:id]).invalidate(15)
64
+ @apple_dispatcher.invalidate(15)
65
+ end
66
+
67
+ it "should delegate to given strategy" do
68
+ mock(@apple_dispatcher[:id]).invalidate(15)
69
+ mock(@apple_dispatcher[:store_id]).invalidate(31)
70
+ @apple_dispatcher.invalidate(:id, 15)
71
+ @apple_dispatcher.invalidate(:store_id, 31)
72
+ end
73
+
74
+ it "should invalidate the request cache" do
75
+ store_dispatcher = Store.record_cache
76
+ mock(store_dispatcher[:request_cache]).invalidate(15)
77
+ store_dispatcher.invalidate(:id, 15)
78
+ end
79
+
80
+ it "should even invalidate the request cache if the given strategy is not known" do
81
+ store_dispatcher = Store.record_cache
82
+ mock(store_dispatcher[:request_cache]).invalidate(31)
83
+ store_dispatcher.invalidate(:unknown_id, 31)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe RecordCache::MultiRead do
4
+
5
+ it "should not delegate to single reads when multi_read is supported" do
6
+ class MultiReadSupported
7
+ def read(key) "single" end
8
+ def read_multi(*keys) "multi" end
9
+ end
10
+ store = RecordCache::MultiRead.test(MultiReadSupported.new)
11
+ store.read_multi("key1", "key2").should == "multi"
12
+ end
13
+
14
+ it "should delegate to single reads when multi_read is explicitly disabled" do
15
+ class ExplicitlyDisabled
16
+ def read(key) "single" end
17
+ def read_multi(*keys) "multi" end
18
+ end
19
+ RecordCache::MultiRead.disable(ExplicitlyDisabled)
20
+ store = RecordCache::MultiRead.test(ExplicitlyDisabled.new)
21
+ store.read_multi("key1", "key2").should == {"key1" => "single", "key2" => "single"}
22
+ end
23
+
24
+ it "should delegate to single reads when multi_read throws an error" do
25
+ class MultiReadNotImplemented
26
+ def read(key) "single" end
27
+ def read_multi(*keys) raise NotImplementedError.new("multiread not implemented") end
28
+ end
29
+ store = RecordCache::MultiRead.test(MultiReadNotImplemented.new)
30
+ store.read_multi("key1", "key2").should == {"key1" => "single", "key2" => "single"}
31
+ end
32
+
33
+ it "should delegate to single reads when multi_read is undefined" do
34
+ class MultiReadNotDefined
35
+ def read(key) "single" end
36
+ end
37
+ store = RecordCache::MultiRead.test(MultiReadNotDefined.new)
38
+ store.read_multi("key1", "key2").should == {"key1" => "single", "key2" => "single"}
39
+ end
40
+
41
+ it "should have tested the Version Store" do
42
+ RecordCache::MultiRead.instance_variable_get(:@tested).should include(RecordCache::Base.version_store.instance_variable_get(:@store))
43
+ end
44
+
45
+ it "should have tested all Record Stores" do
46
+ tested_stores = RecordCache::MultiRead.instance_variable_get(:@tested)
47
+ RecordCache::Base.stores.values.each do |record_store|
48
+ tested_stores.should include(record_store)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,148 @@
1
+ require 'spec_helper'
2
+
3
+ describe RecordCache::Query do
4
+ before(:each) do
5
+ @query = RecordCache::Query.new
6
+ end
7
+
8
+ context "wheres" do
9
+ it "should be an empty hash by default" do
10
+ @query.wheres.should == {}
11
+ end
12
+
13
+ it "should fill wheres on instantiation" do
14
+ @query = RecordCache::Query.new({:id => 1})
15
+ @query.wheres.should == {:id => 1}
16
+ end
17
+
18
+ it "should keep track of where clauses" do
19
+ @query.where(:name, "My name")
20
+ @query.where(:id, [1, 2, 3])
21
+ @query.where(:height, 1.75)
22
+ @query.wheres.should == {:name => "My name", :id => [1, 2, 3], :height => 1.75}
23
+ end
24
+
25
+ context "where_ids" do
26
+ it "should return nil if the attribute is not defined" do
27
+ @query.where(:idx, 15)
28
+ @query.where_ids(:id).should == nil
29
+ end
30
+
31
+ it "should return nil if one the value is nil" do
32
+ @query.where(:id, nil)
33
+ @query.where_ids(:id).should == nil
34
+ end
35
+
36
+ it "should return nil if one of the values is < 1" do
37
+ @query.where(:id, [2, 0, 8])
38
+ @query.where_ids(:id).should == nil
39
+ end
40
+
41
+ it "should return nil if one of the values is nil" do
42
+ @query.where(:id, ["1", nil, "3"])
43
+ @query.where_ids(:id).should == nil
44
+ end
45
+
46
+ it "should retrieve an array of integers when a single integer is provided" do
47
+ @query.where(:id, 15)
48
+ @query.where_ids(:id).should == [15]
49
+ end
50
+
51
+ it "should retrieve an array of integers when a multiple integers are provided" do
52
+ @query.where(:id, [2, 4, 8])
53
+ @query.where_ids(:id).should == [2, 4, 8]
54
+ end
55
+
56
+ it "should retrieve an array of integers when a single string is provided" do
57
+ @query.where(:id, "15")
58
+ @query.where_ids(:id).should == [15]
59
+ end
60
+
61
+ it "should retrieve an array of integers when a multiple strings are provided" do
62
+ @query.where(:id, ["2", "4", "8"])
63
+ @query.where_ids(:id).should == [2, 4, 8]
64
+ end
65
+
66
+ it "should cache the array of integers" do
67
+ @query.where(:id, ["2", "4", "8"])
68
+ ids1 = @query.where_ids(:id)
69
+ ids2 = @query.where_ids(:id)
70
+ ids1.object_id.should == ids2.object_id
71
+ end
72
+ end
73
+
74
+ context "where_id" do
75
+ it "should return nil when multiple integers are provided" do
76
+ @query.where(:id, [2, 4, 8])
77
+ @query.where_id(:id).should == nil
78
+ end
79
+
80
+ it "should return the id when a single integer is provided" do
81
+ @query.where(:id, 4)
82
+ @query.where_id(:id).should == 4
83
+ end
84
+
85
+ it "should return the id when a single string is provided" do
86
+ @query.where(:id, ["4"])
87
+ @query.where_id(:id).should == 4
88
+ end
89
+ end
90
+ end
91
+
92
+ context "sort" do
93
+ it "should be an empty array by default" do
94
+ @query.sort_orders.should == []
95
+ end
96
+
97
+ it "should keep track of sort orders" do
98
+ @query.order_by("name", true)
99
+ @query.order_by("id", false)
100
+ @query.sort_orders.should == [ ["name", true], ["id", false] ]
101
+ end
102
+
103
+ it "should convert attribute to string" do
104
+ @query.order_by(:name, true)
105
+ @query.sort_orders.should == [ ["name", true] ]
106
+ end
107
+
108
+ it "should define sorted?" do
109
+ @query.sorted?.should == false
110
+ @query.order_by("name", true)
111
+ @query.sorted?.should == true
112
+ end
113
+ end
114
+
115
+ context "limit" do
116
+ it "should be +nil+ by default" do
117
+ @query.limit.should == nil
118
+ end
119
+
120
+ it "should keep track of limit" do
121
+ @query.limit = 4
122
+ @query.limit.should == 4
123
+ end
124
+
125
+ it "should convert limit to integer" do
126
+ @query.limit = "4"
127
+ @query.limit.should == 4
128
+ end
129
+ end
130
+
131
+ context "utility" do
132
+ before(:each) do
133
+ @query.where(:name, "My name")
134
+ @query.where(:id, [1, 2, 3])
135
+ @query.order_by("name", true)
136
+ @query.limit = "4"
137
+ end
138
+
139
+ it "should generate a unique key for (request) caching purposes" do
140
+ @query.cache_key.should == 'name="My name"&id=[1, 2, 3].name=AL4'
141
+ end
142
+
143
+ it "should generate a pretty formatted query" do
144
+ @query.to_s.should == 'SELECT name = "My name" AND id = [1, 2, 3] ORDER_BY name ASC LIMIT 4'
145
+ end
146
+ end
147
+
148
+ end
@@ -0,0 +1,140 @@
1
+ require 'spec_helper'
2
+
3
+ describe RecordCache::Statistics do
4
+ before(:each) do
5
+ # remove active setting from other tests
6
+ RecordCache::Statistics.send(:remove_instance_variable, :@active) if RecordCache::Statistics.instance_variable_get(:@active)
7
+ end
8
+
9
+ context "active" do
10
+ it "should default to false" do
11
+ RecordCache::Statistics.active?.should == false
12
+ end
13
+
14
+ it "should be activated by start" do
15
+ RecordCache::Statistics.start
16
+ RecordCache::Statistics.active?.should == true
17
+ end
18
+
19
+ it "should be deactivated by stop" do
20
+ RecordCache::Statistics.start
21
+ RecordCache::Statistics.active?.should == true
22
+ RecordCache::Statistics.stop
23
+ RecordCache::Statistics.active?.should == false
24
+ end
25
+
26
+ it "should be toggleable" do
27
+ RecordCache::Statistics.toggle
28
+ RecordCache::Statistics.active?.should == true
29
+ RecordCache::Statistics.toggle
30
+ RecordCache::Statistics.active?.should == false
31
+ end
32
+ end
33
+
34
+ context "find" do
35
+ it "should return {} for unknown base classes" do
36
+ class UnknownBase; end
37
+ RecordCache::Statistics.find(UnknownBase).should == {}
38
+ end
39
+
40
+ it "should create a new counter for unknown strategies" do
41
+ class UnknownBase; end
42
+ counter = RecordCache::Statistics.find(UnknownBase, :strategy)
43
+ counter.calls.should == 0
44
+ end
45
+
46
+ it "should retrieve all strategies if only the base is provided" do
47
+ class KnownBase; end
48
+ counter1 = RecordCache::Statistics.find(KnownBase, :strategy1)
49
+ counter2 = RecordCache::Statistics.find(KnownBase, :strategy2)
50
+ counters = RecordCache::Statistics.find(KnownBase)
51
+ counters.size.should == 2
52
+ counters[:strategy1].should == counter1
53
+ counters[:strategy2].should == counter2
54
+ end
55
+
56
+ it "should retrieve the counter for an existing strategy" do
57
+ class KnownBase; end
58
+ counter1 = RecordCache::Statistics.find(KnownBase, :strategy1)
59
+ RecordCache::Statistics.find(KnownBase, :strategy1).should == counter1
60
+ end
61
+ end
62
+
63
+ context "reset!" do
64
+ before(:each) do
65
+ class BaseA; end
66
+ @counter_a1 = RecordCache::Statistics.find(BaseA, :strategy1)
67
+ @counter_a2 = RecordCache::Statistics.find(BaseA, :strategy2)
68
+ class BaseB; end
69
+ @counter_b1 = RecordCache::Statistics.find(BaseB, :strategy1)
70
+ end
71
+
72
+ it "should reset all counters for a specific base" do
73
+ mock(@counter_a1).reset!
74
+ mock(@counter_a2).reset!
75
+ mock(@counter_b1).reset!.times(0)
76
+ RecordCache::Statistics.reset!(BaseA)
77
+ end
78
+
79
+ it "should reset all counters" do
80
+ mock(@counter_a1).reset!
81
+ mock(@counter_a2).reset!
82
+ mock(@counter_b1).reset!
83
+ RecordCache::Statistics.reset!
84
+ end
85
+ end
86
+
87
+ context "counter" do
88
+ before(:each) do
89
+ @counter = RecordCache::Statistics::Counter.new
90
+ end
91
+
92
+ it "should be empty by default" do
93
+ [@counter.calls, @counter.hits, @counter.misses].should == [0, 0, 0]
94
+ end
95
+
96
+ it "should delegate active? to RecordCache::Statistics" do
97
+ mock(RecordCache::Statistics).active?
98
+ @counter.active?
99
+ end
100
+
101
+ it "should add hits and misses" do
102
+ @counter.add(4, 3)
103
+ [@counter.calls, @counter.hits, @counter.misses].should == [1, 3, 1]
104
+ end
105
+
106
+ it "should sum added hits and misses" do
107
+ @counter.add(4, 3)
108
+ @counter.add(1, 1)
109
+ @counter.add(3, 2)
110
+ [@counter.calls, @counter.hits, @counter.misses].should == [3, 6, 2]
111
+ end
112
+
113
+ it "should reset! hits and misses" do
114
+ @counter.add(4, 3)
115
+ @counter.add(1, 1)
116
+ @counter.reset!
117
+ [@counter.calls, @counter.hits, @counter.misses].should == [0, 0, 0]
118
+ end
119
+
120
+ it "should provide 0.0 percentage for empty counter" do
121
+ @counter.percentage.should == 0.0
122
+ end
123
+
124
+ it "should provide percentage" do
125
+ @counter.add(4, 3)
126
+ @counter.percentage.should == 75.0
127
+ @counter.add(1, 1)
128
+ @counter.percentage.should == 80.0
129
+ @counter.add(5, 2)
130
+ @counter.percentage.should == 60.0
131
+ end
132
+
133
+ it "should pretty print on inspect" do
134
+ @counter.add(4, 3)
135
+ @counter.add(1, 1)
136
+ @counter.add(5, 2)
137
+ @counter.inspect.should == "60.0% (6/10)"
138
+ end
139
+ end
140
+ end