identity_cache 0.0.1

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.
@@ -0,0 +1,3 @@
1
+ module IdentityCache
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,71 @@
1
+ require 'monitor'
2
+
3
+ module IdentityCache
4
+ class MemoizedCacheProxy
5
+ attr_writer :memcache
6
+
7
+ def initialize(memcache = nil)
8
+ @memcache = memcache || Rails.cache
9
+ @key_value_maps = Hash.new {|h, k| h[k] = {} }
10
+ end
11
+
12
+ def memoized_key_values
13
+ @key_value_maps[Thread.current.object_id]
14
+ end
15
+
16
+ def with_memoization(&block)
17
+ Thread.current[:memoizing_idc] = true
18
+ yield
19
+ ensure
20
+ clear_memoization
21
+ Thread.current[:memoizing_idc] = false
22
+ end
23
+
24
+ def write(key, value)
25
+ memoized_key_values[key] = value if memoizing?
26
+ @memcache.write(key, value)
27
+ end
28
+
29
+ def read(key)
30
+ if memoizing?
31
+ memoized_key_values[key] ||= @memcache.read(key)
32
+ else
33
+ @memcache.read(key)
34
+ end
35
+ end
36
+
37
+ def delete(key)
38
+ memoized_key_values.delete(key) if memoizing?
39
+ @memcache.delete(key)
40
+ end
41
+
42
+ def read_multi(*keys)
43
+ hash = {}
44
+
45
+ if memoizing?
46
+ keys.reduce({}) do |hash, key|
47
+ hash[key] = memoized_key_values[key] if memoized_key_values[key].present?
48
+ hash
49
+ end
50
+ end
51
+
52
+ missing_keys = keys - hash.keys
53
+ hash.merge(@memcache.read_multi(*missing_keys))
54
+ end
55
+
56
+ def clear
57
+ clear_memoization
58
+ @memcache.clear
59
+ end
60
+
61
+ private
62
+
63
+ def clear_memoization
64
+ @key_value_maps.delete(Thread.current.object_id)
65
+ end
66
+
67
+ def memoizing?
68
+ Thread.current[:memoizing_idc]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,73 @@
1
+ require "test_helper"
2
+
3
+ class AttributeCacheTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ AssociatedRecord.cache_attribute :name
7
+ AssociatedRecord.cache_attribute :record, :by => [:id, :name]
8
+
9
+ @parent = Record.create!(:title => 'bob')
10
+ @record = @parent.associated_records.create!(:name => 'foo')
11
+ @name_attribute_key = "IDC:attribute:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s)}"
12
+ @blob_key = "IDC:blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:1"
13
+ end
14
+
15
+ def test_attribute_values_are_returned_on_cache_hits
16
+ IdentityCache.cache.expects(:read).with(@name_attribute_key).returns('foo')
17
+ assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
18
+ end
19
+
20
+ def test_attribute_values_are_fetched_and_returned_on_cache_misses
21
+ IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
22
+ Record.connection.expects(:select_value).with("SELECT name FROM associated_records WHERE id = 1 LIMIT 1").returns('foo')
23
+ assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
24
+ end
25
+
26
+ def test_attribute_values_are_stored_in_the_cache_on_cache_misses
27
+
28
+ # Cache miss, so
29
+ IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
30
+
31
+ # Grab the value of the attribute from the DB
32
+ Record.connection.expects(:select_value).with("SELECT name FROM associated_records WHERE id = 1 LIMIT 1").returns('foo')
33
+
34
+ # And write it back to the cache
35
+ IdentityCache.cache.expects(:write).with(@name_attribute_key, 'foo').returns(nil)
36
+
37
+ assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
38
+ end
39
+
40
+ def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_saved
41
+ IdentityCache.cache.expects(:delete).with(@name_attribute_key)
42
+ IdentityCache.cache.expects(:delete).with(@blob_key)
43
+ @record.save!
44
+ end
45
+
46
+ def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_with_changed_attributes_is_saved
47
+ IdentityCache.cache.expects(:delete).with(@name_attribute_key)
48
+ IdentityCache.cache.expects(:delete).with(@blob_key)
49
+ @record.name = 'bar'
50
+ @record.save!
51
+ end
52
+
53
+ def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_destroyed
54
+ IdentityCache.cache.expects(:delete).with(@name_attribute_key)
55
+ IdentityCache.cache.expects(:delete).with(@blob_key)
56
+ @record.destroy
57
+ end
58
+
59
+ def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
60
+ IdentityCache.cache.expects(:delete).with("IDC:blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:2")
61
+ @parent.associated_records.create(:name => 'bar')
62
+ end
63
+
64
+ def test_fetching_by_attribute_delegates_to_block_if_transactions_are_open
65
+ IdentityCache.cache.expects(:read).with(@name_attribute_key).never
66
+
67
+ Record.connection.expects(:select_value).with("SELECT name FROM associated_records WHERE id = 1 LIMIT 1").returns('foo')
68
+
69
+ @record.transaction do
70
+ assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,89 @@
1
+ require "test_helper"
2
+
3
+ class DenormalizedHasManyTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ Record.cache_has_many :associated_records, :embed => true
7
+
8
+ @record = Record.new(:title => 'foo')
9
+ @record.associated_records << AssociatedRecord.new(:name => 'bar')
10
+ @record.associated_records << AssociatedRecord.new(:name => 'baz')
11
+ @record.save
12
+ @record.reload
13
+ end
14
+
15
+ def test_uncached_record_from_the_db_will_use_normal_association
16
+ expected = @record.associated_records
17
+ record_from_db = Record.find(@record.id)
18
+
19
+ Record.any_instance.expects(:associated_records).returns(expected)
20
+
21
+ assert_equal @record, record_from_db
22
+ assert_equal expected, record_from_db.fetch_associated_records
23
+ end
24
+
25
+ def test_on_cache_hit_record_should_come_back_with_cached_association
26
+ Record.fetch(@record.id) # warm cache
27
+
28
+ record_from_cache_hit = Record.fetch(@record.id)
29
+ assert_equal @record, record_from_cache_hit
30
+
31
+ expected = @record.associated_records
32
+ Record.any_instance.expects(:associated_records).never
33
+ assert_equal expected, record_from_cache_hit.fetch_associated_records
34
+ end
35
+
36
+ def test_on_cache_miss_record_should_embed_associated_objects_and_return
37
+ record_from_cache_miss = Record.fetch(@record.id)
38
+ expected = @record.associated_records
39
+
40
+ assert_equal @record, record_from_cache_miss
41
+ assert_equal expected, record_from_cache_miss.fetch_associated_records
42
+ end
43
+
44
+ def test_changes_in_associated_records_should_expire_the_parents_cache
45
+ Record.fetch(@record.id)
46
+ key = @record.primary_cache_index_key
47
+ assert_not_nil IdentityCache.cache.read(key)
48
+
49
+ IdentityCache.cache.expects(:delete).with(key)
50
+ @record.associated_records.first.save
51
+ end
52
+
53
+ def test_changes_in_associated_records_foreign_keys_should_expire_new_parent_and_old_parents_cache
54
+ @associatated_record = @record.associated_records.first
55
+ old_key = @record.primary_cache_index_key
56
+ @new_record = Record.create
57
+ new_key = @new_record.primary_cache_index_key
58
+
59
+ IdentityCache.cache.expects(:delete).with(old_key)
60
+ IdentityCache.cache.expects(:delete).with(new_key)
61
+ @associatated_record.record_id = @new_record.id
62
+ @associatated_record.save!
63
+ end
64
+
65
+ def test_cached_associations_after_commit_hook_will_not_fail_on_undefined_parent_association
66
+ ar = AssociatedRecord.new
67
+ ar.save
68
+ assert_nothing_raised { ar.expire_parent_cache }
69
+ end
70
+
71
+ def test_cache_without_guessable_inverse_name_raises
72
+ assert_raises IdentityCache::InverseAssociationError do
73
+ Record.cache_has_many :polymorphic_records, :embed => true
74
+ end
75
+ end
76
+
77
+ def test_cache_without_guessable_inverse_name_does_not_raise_when_inverse_name_specified
78
+ assert_nothing_raised do
79
+ Record.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
80
+ end
81
+ end
82
+
83
+ def test_touching_associated_records_should_expire_itself_and_the_parents_cache
84
+ child = @record.associated_records.first
85
+ IdentityCache.cache.expects(:delete).with(child.primary_cache_index_key).once
86
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
87
+ child.touch
88
+ end
89
+ end
@@ -0,0 +1,99 @@
1
+ require "test_helper"
2
+
3
+ class DenormalizedHasOneTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ Record.cache_has_one :associated
7
+ Record.cache_index :title, :unique => true
8
+ @record = Record.new(:title => 'foo')
9
+ @record.associated = AssociatedRecord.new(:name => 'bar')
10
+ @record.save
11
+
12
+ @record.reload
13
+ end
14
+
15
+ def test_on_cache_miss_record_should_embed_associated_object
16
+ IdentityCache.cache.expects(:read).with(@record.secondary_cache_index_key_for_current_values([:title]))
17
+ IdentityCache.cache.expects(:read).with(@record.primary_cache_index_key)
18
+
19
+ record_from_cache_miss = Record.fetch_by_title('foo')
20
+
21
+ assert_equal @record, record_from_cache_miss
22
+ assert_not_nil @record.fetch_associated
23
+ assert_equal @record.associated, record_from_cache_miss.fetch_associated
24
+ end
25
+
26
+ def test_on_cache_miss_record_should_embed_nil_object
27
+ @record.expects(:associated => nil)
28
+ Record.expects(:find_by_id).with(@record.id, :include => Record.cache_fetch_includes).returns(@record)
29
+ IdentityCache.cache.expects(:read).with(@record.secondary_cache_index_key_for_current_values([:title]))
30
+ IdentityCache.cache.expects(:read).with(@record.primary_cache_index_key)
31
+
32
+ record_from_cache_miss = Record.fetch_by_title('foo')
33
+ record_from_cache_miss.expects(:associated).never
34
+
35
+ assert_equal @record, record_from_cache_miss
36
+ 5.times do
37
+ assert_nil record_from_cache_miss.fetch_associated
38
+ end
39
+ end
40
+
41
+ def test_on_record_from_the_db_will_use_normal_association
42
+ record_from_db = Record.find_by_title('foo')
43
+
44
+ assert_equal @record, record_from_db
45
+ assert_not_nil record_from_db.fetch_associated
46
+ end
47
+
48
+ def test_on_cache_hit_record_should_come_back_with_cached_association
49
+ Record.expects(:find_by_id).with(1, :include => Record.cache_fetch_includes).once.returns(@record)
50
+ Record.fetch_by_title('foo')
51
+
52
+ record_from_cache_hit = Record.fetch_by_title('foo')
53
+ expected = @record.associated
54
+
55
+ assert_equal @record, record_from_cache_hit
56
+ assert_equal expected, record_from_cache_hit.fetch_associated
57
+ end
58
+
59
+ def test_on_cache_hit_record_should_come_back_with_cached_nil_association
60
+ @record.expects(:associated => nil)
61
+ Record.expects(:find_by_id).with(1, :include => Record.cache_fetch_includes).once.returns(@record)
62
+ Record.fetch_by_title('foo')
63
+
64
+ record_from_cache_hit = Record.fetch_by_title('foo')
65
+ record_from_cache_hit.expects(:associated).never
66
+
67
+ assert_equal @record, record_from_cache_hit
68
+ 5.times do
69
+ assert_nil record_from_cache_hit.fetch_associated
70
+ end
71
+ end
72
+
73
+ def test_changes_in_associated_record_should_expire_the_parents_cache
74
+ Record.fetch_by_title('foo')
75
+ key = @record.primary_cache_index_key
76
+ assert_not_nil IdentityCache.cache.read(key)
77
+
78
+ IdentityCache.cache.expects(:delete).at_least(1).with(key)
79
+ @record.associated.save
80
+ end
81
+
82
+ def test_cached_associations_after_commit_hook_will_not_fail_on_undefined_parent_association
83
+ ar = AssociatedRecord.new
84
+ ar.save
85
+ assert_nothing_raised { ar.expire_parent_cache }
86
+ end
87
+
88
+ def test_cache_without_guessable_inverse_name_raises
89
+ assert_raises IdentityCache::InverseAssociationError do
90
+ Record.cache_has_one :polymorphic_record, :embed => true
91
+ end
92
+ end
93
+
94
+ def test_cache_without_guessable_inverse_name_does_not_raise_when_inverse_name_specified
95
+ assert_nothing_raised do
96
+ Record.cache_has_one :polymorphic_record, :inverse_name => :owner, :embed => true
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,144 @@
1
+ require "test_helper"
2
+
3
+ class FetchMultiTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ @bob = Record.create!(:title => 'bob')
7
+ @joe = Record.create!(:title => 'joe')
8
+ @fred = Record.create!(:title => 'fred')
9
+ @bob_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:1"
10
+ @joe_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:2"
11
+ @fred_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:3"
12
+ @tenth_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:10"
13
+ end
14
+
15
+ def test_fetch_multi_with_no_records
16
+ assert_equal [], Record.fetch_multi
17
+ end
18
+
19
+ def test_fetch_multi_with_all_hits
20
+ cache_response = {}
21
+ cache_response[@bob_blob_key] = @bob
22
+ cache_response[@joe_blob_key] = @joe
23
+ cache_response[@fred_blob_key] = @fred
24
+ IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
25
+ assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
26
+ end
27
+
28
+ def test_fetch_multi_with_all_misses
29
+ cache_response = {}
30
+ cache_response[@bob_blob_key] = nil
31
+ cache_response[@joe_blob_key] = nil
32
+ cache_response[@fred_blob_key] = nil
33
+ IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
34
+ assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
35
+ end
36
+
37
+ def test_fetch_multi_with_mixed_hits_and_misses
38
+ cache_response = {}
39
+ cache_response[@bob_blob_key] = @bob
40
+ cache_response[@joe_blob_key] = nil
41
+ cache_response[@fred_blob_key] = @fred
42
+ IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
43
+ assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
44
+ end
45
+
46
+ def test_fetch_multi_with_mixed_hits_and_misses_and_responses_in_the_wrong_order
47
+ cache_response = {}
48
+ cache_response[@bob_blob_key] = nil
49
+ cache_response[@joe_blob_key] = nil
50
+ cache_response[@fred_blob_key] = @fred
51
+ IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @fred_blob_key, @joe_blob_key).returns(cache_response)
52
+ assert_equal [@bob, @fred, @joe], Record.fetch_multi(@bob.id, @fred.id, @joe.id)
53
+ end
54
+
55
+ def test_fetch_multi_with_mixed_hits_and_misses_and_non_existant_keys_1
56
+ populate_only_fred
57
+
58
+ IdentityCache.cache.expects(:read_multi).with(@tenth_blob_key, @bob_blob_key, @joe_blob_key, @fred_blob_key).returns(@cache_response)
59
+ assert_equal [@bob, @joe, @fred], Record.fetch_multi(10, @bob.id, @joe.id, @fred.id)
60
+ end
61
+
62
+ def test_fetch_multi_with_mixed_hits_and_misses_and_non_existant_keys_2
63
+ populate_only_fred
64
+
65
+ IdentityCache.cache.expects(:read_multi).with(@fred_blob_key, @bob_blob_key, @tenth_blob_key, @joe_blob_key).returns(@cache_response)
66
+ assert_equal [@fred, @bob, @joe], Record.fetch_multi(@fred.id, @bob.id, 10, @joe.id)
67
+ end
68
+
69
+
70
+ def test_fetch_multi_works_with_nils
71
+ cache_result = {1 => IdentityCache::CACHED_NIL, 2 => IdentityCache::CACHED_NIL}
72
+ fetch_result = {1 => nil, 2 => nil}
73
+
74
+ IdentityCache.cache.expects(:read_multi).with(1,2).times(2).returns({1 => nil, 2 => nil}, cache_result)
75
+ IdentityCache.cache.expects(:write).with(1, IdentityCache::CACHED_NIL).once
76
+ IdentityCache.cache.expects(:write).with(2, IdentityCache::CACHED_NIL).once
77
+
78
+ results = IdentityCache.fetch_multi(1,2) do |keys|
79
+ [nil, nil]
80
+ end
81
+ assert_equal fetch_result, results
82
+
83
+ results = IdentityCache.fetch_multi(1,2) do |keys|
84
+ flunk "Contents should have been fetched from cache successfully"
85
+ end
86
+
87
+ assert_equal fetch_result, results
88
+ end
89
+
90
+ def test_fetch_multi_duplicate_ids
91
+ assert_equal [@joe, @bob, @joe], Record.fetch_multi(@joe.id, @bob.id, @joe.id)
92
+ end
93
+
94
+ def test_fetch_multi_with_open_transactions_hits_the_database
95
+ Record.connection.expects(:open_transactions).at_least_once.returns(1)
96
+ IdentityCache.cache.expects(:read_multi).never
97
+ assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
98
+ end
99
+
100
+ def test_fetch_multi_with_open_transactions_returns_results_in_the_order_of_the_passed_ids
101
+ Record.connection.expects(:open_transactions).at_least_once.returns(1)
102
+ assert_equal [@joe, @bob, @fred], Record.fetch_multi(@joe.id, @bob.id, @fred.id)
103
+ end
104
+
105
+ def test_fetch_multi_with_duplicate_ids_in_transaction_returns_results_in_the_order_of_the_passed_ids
106
+ Record.connection.expects(:open_transactions).at_least_once.returns(1)
107
+ assert_equal [@joe, @bob, @joe], Record.fetch_multi(@joe.id, @bob.id, @joe.id)
108
+ end
109
+
110
+ def test_fetch_multi_includes_cached_associations
111
+ Record.send(:cache_has_many, :associated_records, :embed => true)
112
+ Record.send(:cache_has_one, :associated)
113
+
114
+ cache_response = {}
115
+ cache_response[@bob_blob_key] = nil
116
+ cache_response[@joe_blob_key] = nil
117
+ cache_response[@fred_blob_key] = nil
118
+
119
+ IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
120
+
121
+ mock_relation = mock("ActiveRecord::Relation")
122
+ Record.expects(:where).returns(mock_relation)
123
+ mock_relation.expects(:includes).with([:associated_records, :associated]).returns(stub(:all => [@bob, @joe, @fred]))
124
+ assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
125
+ end
126
+
127
+ def test_find_batch_coerces_ids_to_primary_key_type
128
+ mock_relation = mock("ActiveRecord::Relation")
129
+ Record.expects(:where).returns(mock_relation)
130
+ mock_relation.expects(:includes).returns(stub(:all => [@bob, @joe, @fred]))
131
+
132
+ Record.find_batch([@bob, @joe, @fred].map(&:id).map(&:to_s))
133
+ end
134
+
135
+ private
136
+
137
+ def populate_only_fred
138
+ @cache_response = {}
139
+ @cache_response[@bob_blob_key] = nil
140
+ @cache_response[@joe_blob_key] = nil
141
+ @cache_response[@tenth_blob_key] = nil
142
+ @cache_response[@fred_blob_key] = @fred
143
+ end
144
+ end