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.
@@ -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 = IdentityCache.fetch(rails_cache_key(id)){ resolve_cache_miss(id) }
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.class.name} with ID=#{id}")
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
- objects_by_key = IdentityCache.fetch_multi(*cache_keys) do |unresolved_keys|
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| objects_by_key[key] }.compact
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
- extra_keys = if respond_to? :updated_at
268
- old_updated_at = old_values_for_fields([:updated_at]).first
269
- "expiring_last_updated_at=#{old_updated_at}"
270
- else
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
@@ -1,3 +1,4 @@
1
1
  module IdentityCache
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
+ CACHE_VERSION = 3
3
4
  end
@@ -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
- i = 1
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
- class FetchMissRunner < CacheRunner
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
- i = 1
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
- i+=1
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
- class FetchHitRunner < CacheRunner
116
+ module HitRunner
103
117
  def prepare
104
118
  IdentityCache.cache.clear
105
- i = 1
106
- @count.times do
107
- ::Record.fetch(i)
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
- i = 1
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
- i+=1
148
+ rec.fetch_normalized_associated_records
120
149
  end
121
150
  end
122
151
  end
data/performance/cpu.rb CHANGED
@@ -25,4 +25,7 @@ Benchmark.bmbm do |x|
25
25
 
26
26
  run(FetchHitRunner.new(RUNS), x)
27
27
 
28
+ run(DoubleFetchHitRunner.new(RUNS), x)
29
+
30
+ run(DoubleFetchMissRunner.new(RUNS), x)
28
31
  end
@@ -43,3 +43,7 @@ run(FindRunner.new(RUNS))
43
43
  run(FetchHitRunner.new(RUNS))
44
44
 
45
45
  run(FetchMissRunner.new(RUNS))
46
+
47
+ run(DoubleFetchHitRunner.new(RUNS))
48
+
49
+ run(DoubleFetchMissRunner.new(RUNS))
@@ -24,3 +24,7 @@ run(FindRunner.new(RUNS))
24
24
  run(FetchMissRunner.new(RUNS))
25
25
 
26
26
  run(FetchHitRunner.new(RUNS))
27
+
28
+ run(DoubleFetchHitRunner.new(RUNS))
29
+
30
+ run(DoubleFetchMissRunner.new(RUNS))
@@ -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 = "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
+ @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("IDC:blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:2")
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
@@ -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 = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:1"
10
- @joe_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:2"
11
- @fred_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:3"
12
- @tenth_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:10"
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