identity_cache 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +13 -5
- data/.travis.yml +4 -2
- data/CHANGELOG +5 -0
- data/README.md +9 -1
- data/Rakefile +5 -1
- data/identity_cache.gemspec +14 -6
- data/lib/identity_cache.rb +17 -34
- data/lib/identity_cache/cache_hash.rb +36 -0
- data/lib/identity_cache/cache_key_generation.rb +34 -6
- data/lib/identity_cache/configuration_dsl.rb +4 -6
- data/lib/identity_cache/memoized_cache_proxy.rb +19 -17
- data/lib/identity_cache/query_api.rb +105 -11
- data/lib/identity_cache/version.rb +2 -1
- data/performance/cache_runner.rb +44 -15
- data/performance/cpu.rb +3 -0
- data/performance/externals.rb +4 -0
- data/performance/profile.rb +4 -0
- data/test/attribute_cache_test.rb +5 -3
- data/test/cache_hash_test.rb +16 -0
- data/test/fetch_multi_test.rb +32 -13
- data/test/fetch_multi_with_batched_associations_test.rb +6 -4
- data/test/fetch_test.rb +28 -7
- data/test/fixtures/serialized_record +0 -0
- data/test/helpers/active_record_objects.rb +25 -1
- data/test/helpers/database_connection.rb +7 -6
- data/test/helpers/serialization_format.rb +38 -0
- data/test/helpers/update_serialization_format.rb +28 -0
- data/test/index_cache_test.rb +5 -3
- data/test/normalized_has_many_test.rb +10 -0
- data/test/save_test.rb +16 -14
- data/test/schema_change_test.rb +16 -0
- data/test/serialization_format_change_test.rb +16 -0
- data/test/test_helper.rb +1 -1
- metadata +51 -40
@@ -4,6 +4,11 @@ module IdentityCache
|
|
4
4
|
|
5
5
|
included do |base|
|
6
6
|
base.private_class_method :require_if_necessary
|
7
|
+
base.private_class_method :coder_from_record
|
8
|
+
base.private_class_method :record_from_coder
|
9
|
+
base.private_class_method :set_embedded_association
|
10
|
+
base.private_class_method :get_embedded_association
|
11
|
+
base.private_class_method :add_cached_associations_to_coder
|
7
12
|
base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
8
13
|
private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association,
|
9
14
|
:populate_denormalized_cached_association
|
@@ -27,7 +32,9 @@ module IdentityCache
|
|
27
32
|
if IdentityCache.should_cache?
|
28
33
|
|
29
34
|
require_if_necessary do
|
30
|
-
object =
|
35
|
+
object = nil
|
36
|
+
coder = IdentityCache.fetch(rails_cache_key(id)){ coder_from_record(object = resolve_cache_miss(id)) }
|
37
|
+
object ||= record_from_coder(coder)
|
31
38
|
IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]} " if object && object.id != id.to_i
|
32
39
|
object
|
33
40
|
end
|
@@ -41,7 +48,7 @@ module IdentityCache
|
|
41
48
|
# ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
|
42
49
|
# if id is not in the cache or the db.
|
43
50
|
def fetch(id)
|
44
|
-
fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.
|
51
|
+
fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.name} with ID=#{id}")
|
45
52
|
end
|
46
53
|
|
47
54
|
# Default fetcher added to the model on inclusion, if behaves like
|
@@ -55,14 +62,14 @@ module IdentityCache
|
|
55
62
|
cache_keys = ids.map {|id| rails_cache_key(id) }
|
56
63
|
key_to_id_map = Hash[ cache_keys.zip(ids) ]
|
57
64
|
|
58
|
-
|
65
|
+
coders_by_key = IdentityCache.fetch_multi(*cache_keys) do |unresolved_keys|
|
59
66
|
ids = unresolved_keys.map {|key| key_to_id_map[key] }
|
60
67
|
records = find_batch(ids, options)
|
61
68
|
records.compact.each(&:populate_association_caches)
|
62
|
-
records
|
69
|
+
records.map {|record| coder_from_record(record) }
|
63
70
|
end
|
64
71
|
|
65
|
-
records = cache_keys.map {|key|
|
72
|
+
records = cache_keys.map {|key| record_from_coder(coders_by_key[key]) }.compact
|
66
73
|
prefetch_associations(options[:includes], records) if options[:includes]
|
67
74
|
|
68
75
|
records
|
@@ -73,6 +80,82 @@ module IdentityCache
|
|
73
80
|
end
|
74
81
|
end
|
75
82
|
|
83
|
+
def record_from_coder(coder) #:nodoc:
|
84
|
+
if coder.present? && coder.has_key?(:class)
|
85
|
+
record = coder[:class].allocate
|
86
|
+
unless coder[:class].serialized_attributes.empty?
|
87
|
+
coder = coder.dup
|
88
|
+
coder['attributes'] = coder['attributes'].dup
|
89
|
+
end
|
90
|
+
if record.class._initialize_callbacks.empty?
|
91
|
+
record.instance_eval do
|
92
|
+
@attributes = self.class.initialize_attributes(coder['attributes'])
|
93
|
+
@relation = nil
|
94
|
+
|
95
|
+
@attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
|
96
|
+
@association_cache = {}
|
97
|
+
@aggregation_cache = {}
|
98
|
+
@readonly = @destroyed = @marked_for_destruction = false
|
99
|
+
@new_record = false
|
100
|
+
end
|
101
|
+
else
|
102
|
+
record.init_with(coder)
|
103
|
+
end
|
104
|
+
|
105
|
+
coder[:associations].each {|name, value| set_embedded_association(record, name, value) } if coder.has_key?(:associations)
|
106
|
+
coder[:normalized_has_many].each {|name, ids| record.instance_variable_set(:"@#{record.class.cached_has_manys[name][:ids_variable_name]}", ids) } if coder.has_key?(:normalized_has_many)
|
107
|
+
record
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_embedded_association(record, association_name, coder_or_array) #:nodoc:
|
112
|
+
value = if IdentityCache.unmap_cached_nil_for(coder_or_array).nil?
|
113
|
+
nil
|
114
|
+
elsif (reflection = record.class.reflect_on_association(association_name)).collection?
|
115
|
+
association = reflection.association_class.new(record, reflection)
|
116
|
+
association.target = coder_or_array.map {|e| record_from_coder(e) }
|
117
|
+
association.target.each {|e| association.set_inverse_instance(e) }
|
118
|
+
association.proxy
|
119
|
+
else
|
120
|
+
record_from_coder(coder_or_array)
|
121
|
+
end
|
122
|
+
variable_name = record.class.all_embedded_associations[association_name][:records_variable_name]
|
123
|
+
record.instance_variable_set(:"@#{variable_name}", IdentityCache.map_cached_nil_for(value))
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_embedded_association(record, association, options) #:nodoc:
|
127
|
+
embedded_variable = record.instance_variable_get(:"@#{options[:records_variable_name]}")
|
128
|
+
if IdentityCache.unmap_cached_nil_for(embedded_variable).nil?
|
129
|
+
nil
|
130
|
+
elsif record.class.reflect_on_association(association).collection?
|
131
|
+
embedded_variable.map {|e| coder_from_record(e) }
|
132
|
+
else
|
133
|
+
coder_from_record(embedded_variable)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def coder_from_record(record) #:nodoc:
|
138
|
+
unless record.nil?
|
139
|
+
coder = {:class => record.class }
|
140
|
+
record.encode_with(coder)
|
141
|
+
add_cached_associations_to_coder(record, coder)
|
142
|
+
coder
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def add_cached_associations_to_coder(record, coder)
|
147
|
+
if record.class.respond_to?(:all_embedded_associations) && record.class.all_embedded_associations.present?
|
148
|
+
coder[:associations] = record.class.all_embedded_associations.each_with_object({}) do |(name, options), hash|
|
149
|
+
hash[name] = IdentityCache.map_cached_nil_for(get_embedded_association(record, name, options))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
if record.class.respond_to?(:cached_has_manys) && record.class.cached_has_manys.present?
|
153
|
+
coder[:normalized_has_many] = record.class.cached_has_manys.each_with_object({}) do |(name, options), hash|
|
154
|
+
hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
76
159
|
def require_if_necessary #:nodoc:
|
77
160
|
# mem_cache_store returns raw value if unmarshal fails
|
78
161
|
rval = yield
|
@@ -97,6 +180,12 @@ module IdentityCache
|
|
97
180
|
end
|
98
181
|
end
|
99
182
|
|
183
|
+
def all_embedded_associations
|
184
|
+
all_cached_associations.select do |cached_association, options|
|
185
|
+
options[:embed].present?
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
100
189
|
def all_cached_associations
|
101
190
|
(cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
|
102
191
|
end
|
@@ -264,13 +353,18 @@ module IdentityCache
|
|
264
353
|
|
265
354
|
def expire_primary_index # :nodoc:
|
266
355
|
return unless self.class.primary_cache_index_enabled
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
356
|
+
|
357
|
+
IdentityCache.logger.debug do
|
358
|
+
extra_keys =
|
359
|
+
if respond_to?(:updated_at)
|
360
|
+
old_updated_at = old_values_for_fields([:updated_at]).first
|
361
|
+
"expiring_last_updated_at=#{old_updated_at}"
|
362
|
+
else
|
363
|
+
""
|
364
|
+
end
|
365
|
+
|
366
|
+
"[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"
|
272
367
|
end
|
273
|
-
IdentityCache.logger.debug { "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}" }
|
274
368
|
|
275
369
|
IdentityCache.cache.delete(primary_cache_index_key)
|
276
370
|
end
|
data/performance/cache_runner.rb
CHANGED
@@ -37,6 +37,7 @@ def create_database(count)
|
|
37
37
|
# set up associations
|
38
38
|
Record.cache_has_one :associated
|
39
39
|
Record.cache_has_many :associated_records, :embed => true
|
40
|
+
Record.cache_has_many :normalized_associated_records, :embed => false
|
40
41
|
Record.cache_index :title, :unique => :true
|
41
42
|
AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
|
42
43
|
|
@@ -54,6 +55,7 @@ def create_database(count)
|
|
54
55
|
a.associated_records
|
55
56
|
(1..5).each do |j|
|
56
57
|
a.associated_records << AssociatedRecord.new(name: "Has Many #{j} for #{i}")
|
58
|
+
a.normalized_associated_records << NormalizedAssociatedRecord.new(name: "Normalized Has Many #{j} for #{i}")
|
57
59
|
end
|
58
60
|
a.save
|
59
61
|
end
|
@@ -74,49 +76,76 @@ end
|
|
74
76
|
|
75
77
|
class FindRunner < CacheRunner
|
76
78
|
def run
|
77
|
-
|
78
|
-
@count.times do
|
79
|
+
(1..@count).each do |i|
|
79
80
|
::Record.find(i)
|
80
|
-
i+=1
|
81
81
|
end
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
|
85
|
+
module MissRunner
|
86
86
|
def prepare
|
87
87
|
IdentityCache.cache.clear
|
88
88
|
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class FetchMissRunner < CacheRunner
|
92
|
+
include MissRunner
|
89
93
|
|
90
94
|
def run
|
91
|
-
|
92
|
-
@count.times do
|
95
|
+
(1..@count).each do |i|
|
93
96
|
rec = ::Record.fetch(i)
|
94
97
|
rec.fetch_associated
|
95
98
|
rec.fetch_associated_records
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
96
102
|
|
97
|
-
|
103
|
+
class DoubleFetchMissRunner < CacheRunner
|
104
|
+
include MissRunner
|
105
|
+
|
106
|
+
def run
|
107
|
+
(1..@count).each do |i|
|
108
|
+
rec = ::Record.fetch(i)
|
109
|
+
rec.fetch_associated
|
110
|
+
rec.fetch_associated_records
|
111
|
+
rec.fetch_normalized_associated_records
|
98
112
|
end
|
99
113
|
end
|
100
114
|
end
|
101
115
|
|
102
|
-
|
116
|
+
module HitRunner
|
103
117
|
def prepare
|
104
118
|
IdentityCache.cache.clear
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
i+=1
|
119
|
+
(1..@count).each do |i|
|
120
|
+
rec = ::Record.fetch(i)
|
121
|
+
rec.fetch_normalized_associated_records
|
109
122
|
end
|
110
123
|
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class FetchHitRunner < CacheRunner
|
127
|
+
include HitRunner
|
128
|
+
|
129
|
+
def run
|
130
|
+
(1..@count).each do |i|
|
131
|
+
rec = ::Record.fetch(i)
|
132
|
+
# these should all be no cost
|
133
|
+
rec.fetch_associated
|
134
|
+
rec.fetch_associated_records
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class DoubleFetchHitRunner < CacheRunner
|
140
|
+
include HitRunner
|
111
141
|
|
112
142
|
def run
|
113
|
-
|
114
|
-
@count.times do
|
143
|
+
(1..@count).each do |i|
|
115
144
|
rec = ::Record.fetch(i)
|
116
145
|
# these should all be no cost
|
117
146
|
rec.fetch_associated
|
118
147
|
rec.fetch_associated_records
|
119
|
-
|
148
|
+
rec.fetch_normalized_associated_records
|
120
149
|
end
|
121
150
|
end
|
122
151
|
end
|
data/performance/cpu.rb
CHANGED
data/performance/externals.rb
CHANGED
data/performance/profile.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class AttributeCacheTest < IdentityCache::TestCase
|
4
|
+
NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE
|
5
|
+
|
4
6
|
def setup
|
5
7
|
super
|
6
8
|
AssociatedRecord.cache_attribute :name
|
@@ -8,8 +10,8 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
8
10
|
|
9
11
|
@parent = Record.create!(:title => 'bob')
|
10
12
|
@record = @parent.associated_records.create!(:name => 'foo')
|
11
|
-
@name_attribute_key = "
|
12
|
-
@blob_key = "
|
13
|
+
@name_attribute_key = "#{NAMESPACE}attribute:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s)}"
|
14
|
+
@blob_key = "#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:1"
|
13
15
|
end
|
14
16
|
|
15
17
|
def test_attribute_values_are_returned_on_cache_hits
|
@@ -57,7 +59,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
57
59
|
end
|
58
60
|
|
59
61
|
def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
|
60
|
-
IdentityCache.cache.expects(:delete).with("
|
62
|
+
IdentityCache.cache.expects(:delete).with("#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:2")
|
61
63
|
@parent.associated_records.create(:name => 'bar')
|
62
64
|
end
|
63
65
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CacheHashTest < IdentityCache::TestCase
|
4
|
+
|
5
|
+
def test_memcache_hash
|
6
|
+
|
7
|
+
prng = Random.new(Time.now.to_i)
|
8
|
+
3.times do
|
9
|
+
random_str = Array.new(200){rand(36).to_s(36)}.join
|
10
|
+
hash_val = IdentityCache.memcache_hash(random_str)
|
11
|
+
assert hash_val
|
12
|
+
assert_kind_of Numeric, hash_val
|
13
|
+
assert_equal 0, (hash_val >> 64)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/test/fetch_multi_test.rb
CHANGED
@@ -1,26 +1,39 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class FetchMultiTest < IdentityCache::TestCase
|
4
|
+
NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE unless const_defined?(:NAMESPACE)
|
5
|
+
|
4
6
|
def setup
|
5
7
|
super
|
6
8
|
@bob = Record.create!(:title => 'bob')
|
7
9
|
@joe = Record.create!(:title => 'joe')
|
8
10
|
@fred = Record.create!(:title => 'fred')
|
9
|
-
@bob_blob_key = "
|
10
|
-
@joe_blob_key = "
|
11
|
-
@fred_blob_key = "
|
12
|
-
@tenth_blob_key = "
|
11
|
+
@bob_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:1"
|
12
|
+
@joe_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:2"
|
13
|
+
@fred_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:3"
|
14
|
+
@tenth_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:10"
|
13
15
|
end
|
14
16
|
|
15
17
|
def test_fetch_multi_with_no_records
|
16
18
|
assert_equal [], Record.fetch_multi
|
17
19
|
end
|
18
20
|
|
21
|
+
def test_fetch_multi_namespace
|
22
|
+
Record.send(:include, SwitchNamespace)
|
23
|
+
bob_blob_key, joe_blob_key, fred_blob_key = [@bob_blob_key, @joe_blob_key, @fred_blob_key].map { |k| "ns:#{k}" }
|
24
|
+
cache_response = {}
|
25
|
+
cache_response[bob_blob_key] = cache_response_for(@bob)
|
26
|
+
cache_response[joe_blob_key] = cache_response_for(@joe)
|
27
|
+
cache_response[fred_blob_key] = cache_response_for(@fred)
|
28
|
+
IdentityCache.cache.expects(:read_multi).with(bob_blob_key, joe_blob_key, fred_blob_key).returns(cache_response)
|
29
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
30
|
+
end
|
31
|
+
|
19
32
|
def test_fetch_multi_with_all_hits
|
20
33
|
cache_response = {}
|
21
|
-
cache_response[@bob_blob_key] = @bob
|
22
|
-
cache_response[@joe_blob_key] = @joe
|
23
|
-
cache_response[@fred_blob_key] = @fred
|
34
|
+
cache_response[@bob_blob_key] = cache_response_for(@bob)
|
35
|
+
cache_response[@joe_blob_key] = cache_response_for(@joe)
|
36
|
+
cache_response[@fred_blob_key] = cache_response_for(@fred)
|
24
37
|
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
25
38
|
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
26
39
|
end
|
@@ -36,9 +49,9 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
36
49
|
|
37
50
|
def test_fetch_multi_with_mixed_hits_and_misses
|
38
51
|
cache_response = {}
|
39
|
-
cache_response[@bob_blob_key] = @bob
|
52
|
+
cache_response[@bob_blob_key] = cache_response_for(@bob)
|
40
53
|
cache_response[@joe_blob_key] = nil
|
41
|
-
cache_response[@fred_blob_key] = @fred
|
54
|
+
cache_response[@fred_blob_key] = cache_response_for(@fred)
|
42
55
|
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
43
56
|
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
44
57
|
end
|
@@ -47,7 +60,7 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
47
60
|
cache_response = {}
|
48
61
|
cache_response[@bob_blob_key] = nil
|
49
62
|
cache_response[@joe_blob_key] = nil
|
50
|
-
cache_response[@fred_blob_key] = @fred
|
63
|
+
cache_response[@fred_blob_key] = cache_response_for(@fred)
|
51
64
|
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @fred_blob_key, @joe_blob_key).returns(cache_response)
|
52
65
|
assert_equal [@bob, @fred, @joe], Record.fetch_multi(@bob.id, @fred.id, @joe.id)
|
53
66
|
end
|
@@ -117,8 +130,8 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
117
130
|
|
118
131
|
def test_fetch_multi_doesnt_freeze_keys
|
119
132
|
cache_response = {}
|
120
|
-
cache_response[@bob_blob_key] = @bob
|
121
|
-
cache_response[@joe_blob_key] = @fred
|
133
|
+
cache_response[@bob_blob_key] = cache_response_for(@bob)
|
134
|
+
cache_response[@joe_blob_key] = cache_response_for(@fred)
|
122
135
|
|
123
136
|
IdentityCache.expects(:fetch_multi).with{ |*args| args.none?(&:frozen?) }.returns(cache_response)
|
124
137
|
|
@@ -132,6 +145,12 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
132
145
|
@cache_response[@bob_blob_key] = nil
|
133
146
|
@cache_response[@joe_blob_key] = nil
|
134
147
|
@cache_response[@tenth_blob_key] = nil
|
135
|
-
@cache_response[@fred_blob_key] = @fred
|
148
|
+
@cache_response[@fred_blob_key] = cache_response_for(@fred)
|
149
|
+
end
|
150
|
+
|
151
|
+
def cache_response_for(record)
|
152
|
+
coder = {:class => record.class}
|
153
|
+
record.encode_with(coder)
|
154
|
+
coder
|
136
155
|
end
|
137
156
|
end
|