identity_cache 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,108 @@
1
+ require "test_helper"
2
+
3
+ class FetchTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ Record.cache_index :title, :unique => true
7
+ Record.cache_index :id, :title, :unique => true
8
+
9
+ @record = Record.new
10
+ @record.id = 1
11
+ @record.title = 'bob'
12
+ @blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:1"
13
+ @index_key = "IDC:index:Record:title:#{cache_hash('bob')}"
14
+ end
15
+
16
+
17
+ def test_fetch_cache_hit
18
+ IdentityCache.cache.expects(:read).with(@blob_key).returns(@record)
19
+
20
+ assert_equal @record, Record.fetch(1)
21
+ end
22
+
23
+ def test_exists_with_identity_cache_when_cache_hit
24
+ IdentityCache.cache.expects(:read).with(@blob_key).returns(@record)
25
+
26
+ assert Record.exists_with_identity_cache?(1)
27
+ end
28
+
29
+ def test_exists_with_identity_cache_when_cache_miss_and_in_db
30
+ IdentityCache.cache.expects(:read).with(@blob_key).returns(nil)
31
+ Record.expects(:find_by_id).with(1, :include => []).returns(@record)
32
+
33
+ assert Record.exists_with_identity_cache?(1)
34
+ end
35
+
36
+ def test_exists_with_identity_cache_when_cache_miss_and_not_in_db
37
+ IdentityCache.cache.expects(:read).with(@blob_key).returns(nil)
38
+ Record.expects(:find_by_id).with(1, :include => []).returns(nil)
39
+
40
+ assert !Record.exists_with_identity_cache?(1)
41
+ end
42
+
43
+ def test_fetch_miss
44
+ Record.expects(:find_by_id).with(1, :include => []).returns(@record)
45
+
46
+ IdentityCache.cache.expects(:read).with(@blob_key).returns(nil)
47
+ IdentityCache.cache.expects(:write).with(@blob_key, @record)
48
+
49
+ assert_equal @record, Record.fetch(1)
50
+ end
51
+
52
+ def test_fetch_by_id_not_found_should_return_nil
53
+ nonexistent_record_id = 10
54
+ IdentityCache.cache.expects(:write).with(@blob_key + '0', IdentityCache::CACHED_NIL)
55
+
56
+ assert_equal nil, Record.fetch_by_id(nonexistent_record_id)
57
+ end
58
+
59
+ def test_fetch_not_found_should_raise
60
+ nonexistent_record_id = 10
61
+ IdentityCache.cache.expects(:write).with(@blob_key + '0', IdentityCache::CACHED_NIL)
62
+
63
+ assert_raises(ActiveRecord::RecordNotFound) { Record.fetch(nonexistent_record_id) }
64
+ end
65
+
66
+ def test_cached_nil_expiry_on_record_creation
67
+ key = @record.primary_cache_index_key
68
+
69
+ assert_equal nil, Record.fetch_by_id(@record.id)
70
+ assert_equal IdentityCache::CACHED_NIL, IdentityCache.cache.read(key)
71
+
72
+ @record.save!
73
+ assert_nil IdentityCache.cache.read(key)
74
+ end
75
+
76
+ def test_fetch_by_title_hit
77
+ # Read record with title bob
78
+ IdentityCache.cache.expects(:read).with(@index_key).returns(nil)
79
+
80
+ # - not found, use sql, SELECT id FROM records WHERE title = '...' LIMIT 1"
81
+ Record.connection.expects(:select_value).returns(1)
82
+
83
+ # cache sql result
84
+ IdentityCache.cache.expects(:write).with(@index_key, 1)
85
+
86
+ # got id, do memcache lookup on that, hit -> done
87
+ IdentityCache.cache.expects(:read).with(@blob_key).returns(@record)
88
+
89
+ assert_equal @record, Record.fetch_by_title('bob')
90
+ end
91
+
92
+ def test_fetch_by_title_stores_idcnil
93
+ Record.connection.expects(:select_value).once.returns(nil)
94
+ Rails.cache.expects(:write).with(@index_key, IdentityCache::CACHED_NIL)
95
+ Rails.cache.expects(:read).with(@index_key).times(3).returns(nil, IdentityCache::CACHED_NIL, IdentityCache::CACHED_NIL)
96
+ assert_equal nil, Record.fetch_by_title('bob') # select_value => nil
97
+
98
+ assert_equal nil, Record.fetch_by_title('bob') # returns cached nil
99
+ assert_equal nil, Record.fetch_by_title('bob') # returns cached nil
100
+ end
101
+
102
+ def test_fetch_by_bang_method
103
+ Record.connection.expects(:select_value).returns(nil)
104
+ assert_raises ActiveRecord::RecordNotFound do
105
+ Record.fetch_by_title!('bob')
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,60 @@
1
+ module Rails
2
+ class Cache
3
+
4
+ def initialize
5
+ @cache = {}
6
+ end
7
+
8
+ def fetch(e)
9
+ if @cache.key?(e)
10
+ return read(e)
11
+ else
12
+ a = yield
13
+ write(e,a)
14
+ return a
15
+ end
16
+ end
17
+
18
+ def clear
19
+ @cache.clear
20
+ end
21
+
22
+ def write(a,b)
23
+ @cache[a] = b
24
+ end
25
+
26
+ def delete(a)
27
+ @cache.delete(a)
28
+ end
29
+
30
+ def read(a)
31
+ @cache[a]
32
+ end
33
+
34
+ def read_multi(*keys)
35
+ keys.reduce({}) do |hash, key|
36
+ hash[key] = @cache[key]
37
+ hash
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.cache
43
+ @@cache ||= Cache.new
44
+ end
45
+
46
+ class Logger
47
+ def info(string)
48
+ end
49
+
50
+ def debug(string)
51
+ end
52
+
53
+ def error(string)
54
+ end
55
+ end
56
+
57
+ def self.logger
58
+ @logger = Logger.new
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ module DatabaseConnection
2
+ def self.setup
3
+ ActiveRecord::Base.establish_connection(DATABASE_CONFIG)
4
+ ActiveRecord::Base.connection
5
+ rescue
6
+ ActiveRecord::Base.establish_connection(DATABASE_CONFIG.merge('database' => nil))
7
+ ActiveRecord::Base.connection.create_database(DATABASE_CONFIG['database'])
8
+ ActiveRecord::Base.establish_connection(DATABASE_CONFIG)
9
+ end
10
+
11
+ def self.drop_tables
12
+ TABLES.keys.each do |table|
13
+ ActiveRecord::Base.connection.drop_table(table) if ActiveRecord::Base.connection.table_exists?(table)
14
+ end
15
+ end
16
+
17
+ def self.create_tables
18
+ TABLES.each do |table, fields|
19
+ ActiveRecord::Base.connection.create_table(table) do |t|
20
+ fields.each do |column_type, *args|
21
+ t.send(column_type, *args)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ TABLES = {
28
+ :polymorphic_records => [[:string, :owner_type], [:integer, :owner_id], [:timestamps]],
29
+ :deeply_associated_records => [[:string, :name], [:integer, :associated_record_id]],
30
+ :associated_records => [[:string, :name], [:integer, :record_id]],
31
+ :not_cached_records => [[:string, :name], [:integer, :record_id]],
32
+ :records => [[:string, :title], [:timestamps]]
33
+ }
34
+
35
+ DATABASE_CONFIG = {
36
+ 'adapter' => 'mysql2',
37
+ 'database' => 'identity_cache_test',
38
+ 'host' => '127.0.0.1',
39
+ 'username' => 'root'
40
+ }
41
+ end
@@ -0,0 +1,17 @@
1
+ require "test_helper"
2
+
3
+ class IdentityCacheTest < IdentityCache::TestCase
4
+
5
+ class BadModel < ActiveRecord::Base
6
+ end
7
+
8
+ def test_identity_cache_raises_if_loaded_twice
9
+ assert_raises(IdentityCache::AlreadyIncludedError) do
10
+ BadModel.class_eval do
11
+ include IdentityCache
12
+ include IdentityCache
13
+ end
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,96 @@
1
+ require "test_helper"
2
+
3
+ class ExpirationTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ @record = Record.new
7
+ @record.id = 1
8
+ @record.title = 'bob'
9
+ @cache_key = "IDC:index:Record:title:#{cache_hash(@record.title)}"
10
+ end
11
+
12
+ def test_unique_index_caches_nil
13
+ Record.cache_index :title, :unique => true
14
+ assert_equal nil, Record.fetch_by_title('bob')
15
+ assert_equal IdentityCache::CACHED_NIL, IdentityCache.cache.read(@cache_key)
16
+ end
17
+
18
+ def test_unique_index_expired_by_new_record
19
+ Record.cache_index :title, :unique => true
20
+ IdentityCache.cache.write(@cache_key, IdentityCache::CACHED_NIL)
21
+ @record.save!
22
+ assert_equal nil, IdentityCache.cache.read(@cache_key)
23
+ end
24
+
25
+ def test_unique_index_filled_on_fetch_by
26
+ Record.cache_index :title, :unique => true
27
+ @record.save!
28
+ assert_equal @record, Record.fetch_by_title('bob')
29
+ assert_equal @record.id, IdentityCache.cache.read(@cache_key)
30
+ end
31
+
32
+ def test_unique_index_expired_by_updated_record
33
+ Record.cache_index :title, :unique => true
34
+ @record.save!
35
+ IdentityCache.cache.write(@cache_key, @record.id)
36
+
37
+ @record.title = 'robert'
38
+ new_cache_key = "IDC:index:Record:title:#{cache_hash(@record.title)}"
39
+ IdentityCache.cache.write(new_cache_key, IdentityCache::CACHED_NIL)
40
+ @record.save!
41
+ assert_equal nil, IdentityCache.cache.read(@cache_key)
42
+ assert_equal nil, IdentityCache.cache.read(new_cache_key)
43
+ end
44
+
45
+
46
+ def test_non_unique_index_caches_empty_result
47
+ Record.cache_index :title
48
+ assert_equal [], Record.fetch_by_title('bob')
49
+ assert_equal [], IdentityCache.cache.read(@cache_key)
50
+ end
51
+
52
+ def test_non_unique_index_expired_by_new_record
53
+ Record.cache_index :title
54
+ IdentityCache.cache.write(@cache_key, [])
55
+ @record.save!
56
+ assert_equal nil, IdentityCache.cache.read(@cache_key)
57
+ end
58
+
59
+ def test_non_unique_index_filled_on_fetch_by
60
+ Record.cache_index :title
61
+ @record.save!
62
+ assert_equal [@record], Record.fetch_by_title('bob')
63
+ assert_equal [@record.id], IdentityCache.cache.read(@cache_key)
64
+ end
65
+
66
+ def test_non_unique_index_fetches_multiple_records
67
+ Record.cache_index :title
68
+ @record.save!
69
+ record2 = Record.create(:id => 2, :title => 'bob')
70
+
71
+ assert_equal [@record, record2], Record.fetch_by_title('bob')
72
+ assert_equal [1, 2], IdentityCache.cache.read(@cache_key)
73
+ end
74
+
75
+ def test_non_unique_index_expired_by_updating_record
76
+ Record.cache_index :title
77
+ @record.save!
78
+ IdentityCache.cache.write(@cache_key, [@record.id])
79
+
80
+ @record.title = 'robert'
81
+ new_cache_key = "IDC:index:Record:title:#{cache_hash(@record.title)}"
82
+ IdentityCache.cache.write(new_cache_key, [])
83
+ @record.save!
84
+ assert_equal nil, IdentityCache.cache.read(@cache_key)
85
+ assert_equal nil, IdentityCache.cache.read(new_cache_key)
86
+ end
87
+
88
+ def test_non_unique_index_expired_by_destroying_record
89
+ Record.cache_index :title
90
+ @record.save!
91
+ IdentityCache.cache.write(@cache_key, [@record.id])
92
+ @record.destroy
93
+ assert_equal nil, IdentityCache.cache.read(@cache_key)
94
+ end
95
+
96
+ end
@@ -0,0 +1,60 @@
1
+ require "test_helper"
2
+
3
+ class MemoizedCacheProxyTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ IdentityCache.cache_backend = Rails.cache
7
+ end
8
+
9
+ def test_chaging_default_cache
10
+ IdentityCache.cache_backend = ActiveSupport::Cache::MemoryStore.new
11
+ IdentityCache.cache.write('foo', 'bar')
12
+ assert_equal 'bar', IdentityCache.cache.read('foo')
13
+ end
14
+
15
+ def test_read_should_short_circuit_on_memoized_values
16
+ Rails.cache.expects(:read).never
17
+
18
+ IdentityCache.cache.with_memoization do
19
+ IdentityCache.cache.write('foo', 'bar')
20
+ assert_equal 'bar', IdentityCache.cache.read('foo')
21
+ end
22
+ end
23
+
24
+ def test_read_should_try_memcached_on_not_memoized_values
25
+ Rails.cache.expects(:read).with('foo').returns('bar')
26
+
27
+ IdentityCache.cache.with_memoization do
28
+ assert_equal 'bar', IdentityCache.cache.read('foo')
29
+ end
30
+ end
31
+
32
+ def test_write_should_memoize_values
33
+ Rails.cache.expects(:read).never
34
+ Rails.cache.expects(:write).with('foo', 'bar')
35
+
36
+
37
+ IdentityCache.cache.with_memoization do
38
+ IdentityCache.cache.write('foo', 'bar')
39
+ assert_equal 'bar', IdentityCache.cache.read('foo')
40
+ end
41
+ end
42
+
43
+ def test_read_multi_with_partially_memoized_should_read_missing_keys_from_memcache
44
+ IdentityCache.cache.write('foo', 'bar')
45
+ Rails.cache.write('fooz', 'baz')
46
+
47
+ IdentityCache.cache.with_memoization do
48
+ assert_equal({'foo' => 'bar', 'fooz' => 'baz'}, IdentityCache.cache.read_multi('foo', 'fooz'))
49
+ end
50
+ end
51
+
52
+ def test_with_memoization_should_not_clear_rails_cache_when_the_block_ends
53
+ IdentityCache.cache.with_memoization do
54
+ IdentityCache.cache.write('foo', 'bar')
55
+ end
56
+
57
+ assert_equal 'bar', Rails.cache.read('foo')
58
+ end
59
+
60
+ end
@@ -0,0 +1,46 @@
1
+ require "test_helper"
2
+
3
+ class NormalizedBelongsToTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ AssociatedRecord.cache_belongs_to :record, :embed => false
7
+
8
+ @parent_record = Record.new(:title => 'foo')
9
+ @parent_record.associated_records << AssociatedRecord.new(:name => 'bar')
10
+ @parent_record.save
11
+ @parent_record.reload
12
+ @record = @parent_record.associated_records.first
13
+ end
14
+
15
+ def test_fetching_the_association_should_delegate_to_the_normal_association_fetcher_if_any_transactions_are_open
16
+ Record.expects(:fetch_by_id).never
17
+ @record.transaction do
18
+ assert_equal @parent_record, @record.fetch_record
19
+ end
20
+ end
21
+
22
+ def test_fetching_the_association_should_delegate_to_the_normal_association_fetcher_if_the_normal_association_is_loaded
23
+ # Warm the ActiveRecord association
24
+ @record.record
25
+
26
+ Record.expects(:fetch_by_id).never
27
+ assert_equal @parent_record, @record.fetch_record
28
+ end
29
+
30
+ def test_fetching_the_association_should_fetch_the_record_from_identity_cache
31
+ Record.expects(:fetch_by_id).with(@parent_record.id).returns(@parent_record)
32
+ assert_equal @parent_record, @record.fetch_record
33
+ end
34
+
35
+ def test_fetching_the_association_should_assign_the_result_to_the_association_so_that_successive_accesses_are_cached
36
+ Record.expects(:fetch_by_id).with(@parent_record.id).returns(@parent_record)
37
+ @record.fetch_record
38
+ assert @record.association(:record).loaded?
39
+ assert_equal @parent_record, @record.record
40
+ end
41
+
42
+ def test_fetching_the_association_shouldnt_raise_if_the_record_cant_be_found
43
+ Record.expects(:fetch_by_id).with(@parent_record.id).returns(nil)
44
+ assert_equal nil, @record.fetch_record
45
+ end
46
+ end
@@ -0,0 +1,125 @@
1
+ require "test_helper"
2
+
3
+ class NormalizedHasManyTest < IdentityCache::TestCase
4
+ def setup
5
+ super
6
+ Record.cache_has_many :associated_records, :embed => false
7
+
8
+ @record = Record.new(:title => 'foo')
9
+ @record.not_cached_records << NotCachedRecord.new(:name => 'NoCache')
10
+ @record.associated_records << AssociatedRecord.new(:name => 'bar')
11
+ @record.associated_records << AssociatedRecord.new(:name => 'baz')
12
+ @record.save
13
+ @record.reload
14
+ @baz, @bar = @record.associated_records[0], @record.associated_records[1]
15
+ @not_cached = @record.not_cached_records.first
16
+ end
17
+
18
+ def test_a_records_list_of_associated_ids_on_the_parent_record_retains_association_sort_order
19
+ assert_equal [2, 1], @record.associated_record_ids
20
+
21
+ AssociatedRecord.create(name: 'foo', record_id: @record.id)
22
+ @record.reload
23
+ assert_equal [3, 2, 1], @record.associated_record_ids
24
+ end
25
+
26
+ def test_defining_a_denormalized_has_many_cache_caches_the_list_of_associated_ids_on_the_parent_record_during_cache_miss
27
+ fetched_record = Record.fetch(@record.id)
28
+ assert_equal [2, 1], fetched_record.cached_associated_record_ids
29
+ end
30
+
31
+ def test_the_cached_the_list_of_associated_ids_on_the_parent_record_should_not_be_populated_by_default
32
+ assert_nil @record.cached_associated_record_ids
33
+ end
34
+
35
+ def test_fetching_the_association_should_fetch_each_record_by_id
36
+ assert_equal [@baz, @bar], @record.fetch_associated_records
37
+ end
38
+
39
+ def test_fetching_the_association_from_a_identity_cached_record_should_not_re_fetch_the_association_ids
40
+ @record = Record.fetch(@record.id)
41
+ @record.expects(:associated_record_ids).never
42
+ assert_equal [@baz, @bar], @record.fetch_associated_records
43
+ end
44
+
45
+ def test_fetching_the_association_should_delegate_to_the_normal_association_fetcher_if_any_transaction_are_open
46
+ @record = Record.fetch(@record.id)
47
+
48
+ Record.expects(:fetch_multi).never
49
+ @record.transaction do
50
+ assert_equal [@baz, @bar], @record.fetch_associated_records
51
+ end
52
+ end
53
+
54
+ def test_fetching_the_association_should_delegate_to_the_normal_association_fetcher_if_the_normal_association_is_loaded
55
+ # Warm the ActiveRecord association
56
+ @record.associated_records.to_a
57
+
58
+ Record.expects(:fetch_multi).never
59
+ assert_equal [@baz, @bar], @record.fetch_associated_records
60
+ end
61
+
62
+ def test_saving_a_child_record_shouldnt_expire_the_parents_blob_if_the_foreign_key_hasnt_changed
63
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key).never
64
+ @baz.name = 'foo'
65
+ @baz.save!
66
+ assert_equal [@baz.id, @bar.id], Record.fetch(@record.id).cached_associated_record_ids
67
+ assert_equal [@baz, @bar], Record.fetch(@record.id).fetch_associated_records
68
+ end
69
+
70
+ def test_creating_a_child_record_should_expire_the_parents_cache_blob
71
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key).once
72
+ @qux = @record.associated_records.create!(:name => 'qux')
73
+ assert_equal [@qux, @baz, @bar], Record.fetch(@record.id).fetch_associated_records
74
+ end
75
+
76
+ def test_saving_a_child_record_should_expire_the_new_and_old_parents_cache_blob
77
+ @new_record = Record.create
78
+ @baz.record_id = @new_record.id
79
+
80
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key).once
81
+ IdentityCache.cache.expects(:delete).with(@new_record.primary_cache_index_key).once
82
+
83
+ @baz.save!
84
+
85
+ assert_equal [@bar.id], Record.fetch(@record.id).cached_associated_record_ids
86
+ assert_equal [@bar], Record.fetch(@record.id).fetch_associated_records
87
+ end
88
+
89
+ def test_saving_a_child_record_in_a_transaction_should_expire_the_new_and_old_parents_cache_blob
90
+ @new_record = Record.create
91
+ @baz.record_id = @new_record.id
92
+
93
+ @baz.transaction do
94
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key).once
95
+ IdentityCache.cache.expects(:delete).with(@new_record.primary_cache_index_key).once
96
+
97
+ @baz.save!
98
+ @baz.reload
99
+ end
100
+
101
+ assert_equal [@bar.id], Record.fetch(@record.id).cached_associated_record_ids
102
+ assert_equal [@bar], Record.fetch(@record.id).fetch_associated_records
103
+ end
104
+
105
+ def test_destroying_a_child_record_should_expire_the_parents_cache_blob
106
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key).once
107
+ @baz.destroy
108
+ assert_equal [@bar], @record.reload.fetch_associated_records
109
+ end
110
+
111
+ def test_touching_a_child_record_should_expire_only_itself
112
+ IdentityCache.cache.expects(:delete).with(@baz.primary_cache_index_key).once
113
+ @baz.touch
114
+ end
115
+
116
+ def test_touching_child_with_touch_true_on_parent_expires_parent
117
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key).once
118
+ @not_cached.touch
119
+ end
120
+
121
+ def test_saving_child_with_touch_true_on_parent_expires_parent
122
+ IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key).once
123
+ @not_cached.save
124
+ end
125
+ end