record-cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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