identity_cache 0.0.3 → 0.0.4

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.
@@ -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