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.
- data/.gitignore +17 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +231 -0
- data/Rakefile +16 -0
- data/identity_cache.gemspec +24 -0
- data/lib/belongs_to_caching.rb +39 -0
- data/lib/identity_cache.rb +570 -0
- data/lib/identity_cache/version.rb +3 -0
- data/lib/memoized_cache_proxy.rb +71 -0
- data/test/attribute_cache_test.rb +73 -0
- data/test/denormalized_has_many_test.rb +89 -0
- data/test/denormalized_has_one_test.rb +99 -0
- data/test/fetch_multi_test.rb +144 -0
- data/test/fetch_test.rb +108 -0
- data/test/helpers/cache.rb +60 -0
- data/test/helpers/database_connection.rb +41 -0
- data/test/identity_cache_test.rb +17 -0
- data/test/index_cache_test.rb +96 -0
- data/test/memoized_cache_proxy_test.rb +60 -0
- data/test/normalized_belongs_to_test.rb +46 -0
- data/test/normalized_has_many_test.rb +125 -0
- data/test/recursive_denormalized_has_many_test.rb +97 -0
- data/test/save_test.rb +64 -0
- data/test/test_helper.rb +73 -0
- metadata +154 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class RecursiveDenormalizedHasManyTest < IdentityCache::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
|
7
|
+
Record.cache_has_many :associated_records, :embed => true
|
8
|
+
Record.cache_has_one :associated
|
9
|
+
|
10
|
+
@record = Record.new(:title => 'foo')
|
11
|
+
|
12
|
+
@associated_record = AssociatedRecord.new(:name => 'bar')
|
13
|
+
@record.associated_records << AssociatedRecord.new(:name => 'baz')
|
14
|
+
@record.associated_records << @associated_record
|
15
|
+
|
16
|
+
@deeply_associated_record = DeeplyAssociatedRecord.new(:name => "corge")
|
17
|
+
@associated_record.deeply_associated_records << @deeply_associated_record
|
18
|
+
@associated_record.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "qux")
|
19
|
+
|
20
|
+
@record.save
|
21
|
+
@record.reload
|
22
|
+
@associated_record.reload
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_cache_fetch_includes
|
26
|
+
assert_equal [{:associated_records => [:deeply_associated_records]}, :associated => [:deeply_associated_records]], Record.cache_fetch_includes
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_uncached_record_from_the_db_will_use_normal_association_for_deeply_associated_records
|
30
|
+
expected = @associated_record.deeply_associated_records
|
31
|
+
record_from_db = Record.find(@record.id)
|
32
|
+
assert_equal expected, record_from_db.fetch_associated_records[0].fetch_deeply_associated_records
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_on_cache_miss_record_should_embed_associated_objects_and_return
|
36
|
+
record_from_cache_miss = Record.fetch(@record.id)
|
37
|
+
expected = @associated_record.deeply_associated_records
|
38
|
+
|
39
|
+
child_record_from_cache_miss = record_from_cache_miss.fetch_associated_records[0]
|
40
|
+
assert_equal @associated_record, child_record_from_cache_miss
|
41
|
+
assert_equal expected, child_record_from_cache_miss.fetch_deeply_associated_records
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_on_cache_hit_record_should_return_embed_associated_objects
|
45
|
+
Record.fetch(@record.id) # warm cache
|
46
|
+
expected = @associated_record.deeply_associated_records
|
47
|
+
|
48
|
+
Record.any_instance.expects(:associated_records).never
|
49
|
+
AssociatedRecord.any_instance.expects(:deeply_associated_records).never
|
50
|
+
|
51
|
+
record_from_cache_hit = Record.fetch(@record.id)
|
52
|
+
child_record_from_cache_hit = record_from_cache_hit.fetch_associated_records[0]
|
53
|
+
assert_equal @associated_record, child_record_from_cache_hit
|
54
|
+
assert_equal expected, child_record_from_cache_hit.fetch_deeply_associated_records
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_on_cache_miss_child_record_fetch_should_include_nested_associations_to_avoid_n_plus_ones
|
58
|
+
record_from_cache_miss = Record.fetch(@record.id)
|
59
|
+
expected = @associated_record.deeply_associated_records
|
60
|
+
|
61
|
+
assert record_from_cache_miss.fetch_associated_records[0].deeply_associated_records.loaded?
|
62
|
+
assert record_from_cache_miss.fetch_associated_records[1].deeply_associated_records.loaded?
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_saving_child_record_should_expire_parent_record
|
66
|
+
IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
|
67
|
+
IdentityCache.cache.expects(:delete).with(@associated_record.primary_cache_index_key)
|
68
|
+
@associated_record.name = 'different'
|
69
|
+
@associated_record.save!
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_saving_grand_child_record_should_expire_parent_record
|
73
|
+
IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
|
74
|
+
IdentityCache.cache.expects(:delete).with(@associated_record.primary_cache_index_key)
|
75
|
+
IdentityCache.cache.expects(:delete).with(@deeply_associated_record.primary_cache_index_key)
|
76
|
+
@deeply_associated_record.name = 'different'
|
77
|
+
@deeply_associated_record.save!
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
class RecursiveNormalizedHasManyTest < IdentityCache::TestCase
|
83
|
+
def setup
|
84
|
+
super
|
85
|
+
AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
|
86
|
+
Record.cache_has_many :associated_records, :embed => false
|
87
|
+
|
88
|
+
@record = Record.new(:title => 'foo')
|
89
|
+
@record.save
|
90
|
+
@record.reload
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_cache_repopulation_should_not_fetch_non_embedded_associations
|
94
|
+
Record.any_instance.expects(:fetch_associated_records).never
|
95
|
+
record_from_cache_miss = Record.fetch(@record.id)
|
96
|
+
end
|
97
|
+
end
|
data/test/save_test.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SaveTest < 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.create(:title => 'bob')
|
10
|
+
@blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:1"
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_create
|
14
|
+
@record = Record.new
|
15
|
+
@record.title = 'bob'
|
16
|
+
|
17
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:id/title:#{cache_hash('2/bob')}")
|
18
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:title:#{cache_hash('bob')}")
|
19
|
+
IdentityCache.cache.expects(:delete).with("IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:2").once
|
20
|
+
@record.save
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_update
|
24
|
+
# Regular flow, write index id, write index id/tile, delete data blob since Record has changed
|
25
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:id/title:#{cache_hash('1/fred')}")
|
26
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:title:#{cache_hash('fred')}")
|
27
|
+
IdentityCache.cache.expects(:delete).with(@blob_key)
|
28
|
+
|
29
|
+
# Delete index id, delete index id/title
|
30
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:id/title:#{cache_hash('1/bob')}")
|
31
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:title:#{cache_hash('bob')}")
|
32
|
+
|
33
|
+
@record.title = 'fred'
|
34
|
+
@record.save
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_destroy
|
38
|
+
# Regular flow: delete data blob, delete index id, delete index id/tile
|
39
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:id/title:#{cache_hash('1/bob')}")
|
40
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:title:#{cache_hash('bob')}")
|
41
|
+
IdentityCache.cache.expects(:delete).with(@blob_key)
|
42
|
+
|
43
|
+
@record.destroy
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_destroy_with_changed_attributes
|
47
|
+
# Make sure to delete the old cache index key, since the new title never ended up in an index
|
48
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:id/title:#{cache_hash('1/bob')}")
|
49
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:title:#{cache_hash('bob')}")
|
50
|
+
IdentityCache.cache.expects(:delete).with(@blob_key)
|
51
|
+
|
52
|
+
@record.title = 'fred'
|
53
|
+
@record.destroy
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_touch_will_expire_the_caches
|
57
|
+
# Regular flow: delete data blob, delete index id, delete index id/tile
|
58
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:id/title:#{cache_hash('1/bob')}")
|
59
|
+
IdentityCache.cache.expects(:delete).with("IDC:index:Record:title:#{cache_hash('bob')}")
|
60
|
+
IdentityCache.cache.expects(:delete).with(@blob_key)
|
61
|
+
|
62
|
+
@record.touch
|
63
|
+
end
|
64
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'mocha/setup'
|
3
|
+
require 'active_record'
|
4
|
+
require 'helpers/cache'
|
5
|
+
require 'helpers/database_connection'
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../lib/identity_cache'
|
8
|
+
|
9
|
+
DatabaseConnection.setup
|
10
|
+
|
11
|
+
class IdentityCache::TestCase < MiniTest::Unit::TestCase
|
12
|
+
def setup
|
13
|
+
DatabaseConnection.drop_tables
|
14
|
+
DatabaseConnection.create_tables
|
15
|
+
|
16
|
+
setup_models
|
17
|
+
end
|
18
|
+
|
19
|
+
def teardown
|
20
|
+
IdentityCache.cache.clear
|
21
|
+
ActiveSupport::DescendantsTracker.clear
|
22
|
+
ActiveSupport::Dependencies.clear
|
23
|
+
Object.send :remove_const, 'DeeplyAssociatedRecord'
|
24
|
+
Object.send :remove_const, 'PolymorphicRecord'
|
25
|
+
Object.send :remove_const, 'AssociatedRecord'
|
26
|
+
Object.send :remove_const, 'NotCachedRecord'
|
27
|
+
Object.send :remove_const, 'Record'
|
28
|
+
end
|
29
|
+
|
30
|
+
def assert_nothing_raised
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
|
34
|
+
def assert_not_nil(*args)
|
35
|
+
assert *args
|
36
|
+
end
|
37
|
+
|
38
|
+
def cache_hash(key)
|
39
|
+
CityHash.hash64(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def setup_models
|
44
|
+
Object.send :const_set, 'DeeplyAssociatedRecord', Class.new(ActiveRecord::Base).tap {|klass|
|
45
|
+
klass.send :include, IdentityCache
|
46
|
+
klass.belongs_to :associated_record
|
47
|
+
}
|
48
|
+
|
49
|
+
Object.send :const_set, 'AssociatedRecord', Class.new(ActiveRecord::Base).tap {|klass|
|
50
|
+
klass.send :include, IdentityCache
|
51
|
+
klass.belongs_to :record
|
52
|
+
klass.has_many :deeply_associated_records, :order => "name DESC"
|
53
|
+
}
|
54
|
+
|
55
|
+
Object.send :const_set, 'NotCachedRecord', Class.new(ActiveRecord::Base).tap {|klass|
|
56
|
+
klass.belongs_to :record, :touch => true
|
57
|
+
}
|
58
|
+
|
59
|
+
Object.send :const_set, 'PolymorphicRecord', Class.new(ActiveRecord::Base).tap {|klass|
|
60
|
+
klass.belongs_to :owner, :polymorphic => true
|
61
|
+
}
|
62
|
+
|
63
|
+
Object.send :const_set, 'Record', Class.new(ActiveRecord::Base).tap {|klass|
|
64
|
+
klass.send :include, IdentityCache
|
65
|
+
klass.belongs_to :record
|
66
|
+
klass.has_many :associated_records, :order => "id DESC"
|
67
|
+
klass.has_many :not_cached_records, :order => "id DESC"
|
68
|
+
klass.has_many :polymorphic_records, :as => 'owner'
|
69
|
+
klass.has_one :polymorphic_record, :as => 'owner'
|
70
|
+
klass.has_one :associated, :class_name => 'AssociatedRecord', :order => "id ASC"
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: identity_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Camilo Lopez
|
9
|
+
- Tom Burns
|
10
|
+
- Harry Brundage
|
11
|
+
- Dylan Smith
|
12
|
+
- Tobias Lütke
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
date: 2013-03-17 00:00:00.000000000 Z
|
17
|
+
dependencies:
|
18
|
+
- !ruby/object:Gem::Dependency
|
19
|
+
name: ar_transaction_changes
|
20
|
+
requirement: &70221938344660 !ruby/object:Gem::Requirement
|
21
|
+
none: false
|
22
|
+
requirements:
|
23
|
+
- - =
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 0.0.1
|
26
|
+
type: :runtime
|
27
|
+
prerelease: false
|
28
|
+
version_requirements: *70221938344660
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: cityhash
|
31
|
+
requirement: &70221938344160 !ruby/object:Gem::Requirement
|
32
|
+
none: false
|
33
|
+
requirements:
|
34
|
+
- - =
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 0.6.0
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: *70221938344160
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rake
|
42
|
+
requirement: &70221938343780 !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: *70221938343780
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: mocha
|
53
|
+
requirement: &70221938343320 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
type: :development
|
60
|
+
prerelease: false
|
61
|
+
version_requirements: *70221938343320
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: mysql2
|
64
|
+
requirement: &70221938342900 !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: *70221938342900
|
73
|
+
description: Opt in read through ActiveRecord caching.
|
74
|
+
email:
|
75
|
+
- harry.brundage@shopify.com
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- .gitignore
|
81
|
+
- .travis.yml
|
82
|
+
- Gemfile
|
83
|
+
- LICENSE
|
84
|
+
- README.md
|
85
|
+
- Rakefile
|
86
|
+
- identity_cache.gemspec
|
87
|
+
- lib/belongs_to_caching.rb
|
88
|
+
- lib/identity_cache.rb
|
89
|
+
- lib/identity_cache/version.rb
|
90
|
+
- lib/memoized_cache_proxy.rb
|
91
|
+
- test/attribute_cache_test.rb
|
92
|
+
- test/denormalized_has_many_test.rb
|
93
|
+
- test/denormalized_has_one_test.rb
|
94
|
+
- test/fetch_multi_test.rb
|
95
|
+
- test/fetch_test.rb
|
96
|
+
- test/helpers/cache.rb
|
97
|
+
- test/helpers/database_connection.rb
|
98
|
+
- test/identity_cache_test.rb
|
99
|
+
- test/index_cache_test.rb
|
100
|
+
- test/memoized_cache_proxy_test.rb
|
101
|
+
- test/normalized_belongs_to_test.rb
|
102
|
+
- test/normalized_has_many_test.rb
|
103
|
+
- test/recursive_denormalized_has_many_test.rb
|
104
|
+
- test/save_test.rb
|
105
|
+
- test/test_helper.rb
|
106
|
+
homepage: https://github.com/Shopify/identity_cache
|
107
|
+
licenses: []
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
segments:
|
119
|
+
- 0
|
120
|
+
hash: 460007319304896635
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
segments:
|
128
|
+
- 0
|
129
|
+
hash: 460007319304896635
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 1.8.11
|
133
|
+
signing_key:
|
134
|
+
specification_version: 3
|
135
|
+
summary: IdentityCache lets you specify how you want to cache your model objects,
|
136
|
+
at the model level, and adds a number of convenience methods for accessing those
|
137
|
+
objects through the cache. Memcached is used as the backend cache store, and the
|
138
|
+
database is only hit when a copy of the object cannot be found in Memcached.
|
139
|
+
test_files:
|
140
|
+
- test/attribute_cache_test.rb
|
141
|
+
- test/denormalized_has_many_test.rb
|
142
|
+
- test/denormalized_has_one_test.rb
|
143
|
+
- test/fetch_multi_test.rb
|
144
|
+
- test/fetch_test.rb
|
145
|
+
- test/helpers/cache.rb
|
146
|
+
- test/helpers/database_connection.rb
|
147
|
+
- test/identity_cache_test.rb
|
148
|
+
- test/index_cache_test.rb
|
149
|
+
- test/memoized_cache_proxy_test.rb
|
150
|
+
- test/normalized_belongs_to_test.rb
|
151
|
+
- test/normalized_has_many_test.rb
|
152
|
+
- test/recursive_denormalized_has_many_test.rb
|
153
|
+
- test/save_test.rb
|
154
|
+
- test/test_helper.rb
|