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