identity_cache 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 477c35f8ea40ea8d71d6937d63ab1842a7321871
4
- data.tar.gz: 2729aa2c8c4c06dcf42b8be9f167b88aa79cc81b
3
+ metadata.gz: 46228f11af3be0212c16960de9cb1d321ceac8db
4
+ data.tar.gz: bf0fca38334faff8e71ea229bf472570efc969ff
5
5
  SHA512:
6
- metadata.gz: efaef756ca74582dfb7b8669933720f482d95da1493a2f2173e88c8f6cdc79861225858ebced572386e499140f9eb41ecf20a519d4db7c8363e7693141816d81
7
- data.tar.gz: e11dff95ae3b78d4d57fd305eb961c4bd21e40e34a7fd08df8cf529369be841a0a75ededa64a47943e7328d10c045cb698744b070d4da2d5301ea0cdb652f61d
6
+ metadata.gz: bcaccc11973f744fb68e25572a0cb90fafd6ad559b2c54764c61c6fa9d894305b5c59a0ca32c6f01e24bf3e539b5834288641f830e3034b3871cd8aa3204dd6b
7
+ data.tar.gz: dfe2fafe9f1c65a78a972cc0369078a17442e168436896f5600b6d964c2cca6ff3fdd10a0998d63e2f65c7fbe77c22ea5a007b42c40c9ade132d443cd59d3a46
checksums.yaml.gz.sig ADDED
@@ -0,0 +1 @@
1
+ �V�u�UnH�͏Q� �jP4���;wB]����4�ݼ�(���c�4ߟ�j��� `��(�cg�����>
data/.travis.yml CHANGED
@@ -3,7 +3,6 @@ rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
5
  - 2.1.1
6
- - jruby
7
6
  gemfile:
8
7
  - Gemfile32
9
8
  - Gemfile
@@ -11,8 +10,5 @@ gemfile:
11
10
  services:
12
11
  - memcache
13
12
  - mysql
14
- matrix:
15
- allow_failures:
16
- - rvm: jruby
17
13
  before_script:
18
14
  - mysql -e 'create database identity_cache_test'
data/README.md CHANGED
@@ -217,6 +217,10 @@ Since everything is being marshalled and unmarshalled from Memcached changing Ru
217
217
 
218
218
  IdentityCache is also very much _opt-in_ by deliberate design. This means IdentityCache does not mess with the way normal Rails associations work, and including it in a model won't change any clients of that model until you switch them to use `fetch` instead of `find`. This is because there is no way IdentityCache is ever going to be 100% consistent. Processes die, execeptions happen, and network blips occur, which means there is a chance that some database transaction might commit but the corresponding memcached DEL operation does not make it. This means that you need to think carefully about when you use `fetch` and when you use `find`. For example, at Shopify, we never use any `fetch`ers on the path which moves money around, because IdentityCache could simply be wrong, and we want to charge people the right amount of money. We do however use the fetchers on performance critical paths where absolute correctness isn't the most important thing, and this is what IdentityCache is intended for.
219
219
 
220
+ ## Note
221
+
222
+ JRuby will not work with this current version, as we are using the memcached gem internally to interface with memcache.
223
+
220
224
  ## Contributing
221
225
 
222
226
  Caching is hard. Chances are that if some feature was left out, it was left out on purpose because it didn't make sense to cache in that way. This is used in production at Shopify so we are very opinionated about the types of features we're going to add. Please start the discussion early, before even adding code, so that we can talk about the feature you are proposing and decide if it makes sense in IdentityCache.
@@ -243,5 +247,5 @@ Harry Brundage (@hornairs)
243
247
  Dylan Smith (@dylanahsmith)
244
248
  Tobias Lütke (@tobi)
245
249
  John Duff (@jduff)
246
- Francis Bogsany (@fbogsany)
250
+ Francis Bogsanyi (@fbogsany)
247
251
  Arthur Neves (@arthurnn)
@@ -2,7 +2,7 @@
2
2
  require File.expand_path('../lib/identity_cache/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Camilo Lopez", "Tom Burns", "Harry Brundage", "Dylan Smith", "Tobias Lutke", "Arthur Neves"]
5
+ gem.authors = ["Camilo Lopez", "Tom Burns", "Harry Brundage", "Dylan Smith", "Tobias Lutke", "Arthur Neves", "Francis Bogsanyi"]
6
6
  gem.email = ["harry.brundage@shopify.com"]
7
7
  gem.description = %q{Opt in read through ActiveRecord caching.}
8
8
  gem.summary = %q{IdentityCache lets you specify how you want to cache your model objects, at the model level, and adds a number of convenience methods for accessing those objects through the cache. Memcached is used as the backend cache store, and the database is only hit when a copy of the object cannot be found in Memcached.}
@@ -17,16 +17,16 @@ Gem::Specification.new do |gem|
17
17
 
18
18
  gem.add_dependency('ar_transaction_changes', '~> 1.0')
19
19
  gem.add_dependency('activerecord', '>= 3.2')
20
+ gem.add_dependency('memcached', '~> 1.8.0')
20
21
 
21
- gem.add_development_dependency('memcached_store', '~> 0.11.2')
22
+ gem.add_development_dependency('memcached_store', '~> 0.12.5')
22
23
  gem.add_development_dependency('rake')
23
24
  gem.add_development_dependency('mocha', '0.14.0')
24
25
  gem.add_development_dependency('spy')
26
+ gem.add_development_dependency('minitest', '>= 2.11.0')
25
27
 
26
28
  if RUBY_PLATFORM == 'java'
27
- gem.add_development_dependency 'jruby-openssl'
28
- gem.add_development_dependency 'activerecord-jdbcmysql-adapter'
29
- gem.add_development_dependency 'jdbc-mysql'
29
+ raise NotImplementedError
30
30
  else
31
31
  gem.add_development_dependency('cityhash', '0.6.0')
32
32
  gem.add_development_dependency('mysql2')
@@ -0,0 +1,84 @@
1
+ module IdentityCache
2
+ class CacheFetcher
3
+ attr_accessor :cache_backend
4
+
5
+ def initialize(cache_backend = nil)
6
+ @cache_backend = cache_backend || Rails.cache
7
+ end
8
+
9
+ def write(key, value)
10
+ @cache_backend.write(key, value)
11
+ end
12
+
13
+ def delete(key)
14
+ @cache_backend.write(key, IdentityCache::DELETED, :expires_in => IdentityCache::DELETED_TTL.seconds)
15
+ end
16
+
17
+ def clear
18
+ @cache_backend.clear
19
+ end
20
+
21
+ def fetch_multi(keys, &block)
22
+ results = cas_multi(keys, &block)
23
+ results = add_multi(keys, &block) if results.nil?
24
+ results
25
+ end
26
+
27
+ def fetch(key)
28
+ result = nil
29
+ yielded = false
30
+ @cache_backend.cas(key) do |value|
31
+ yielded = true
32
+ unless IdentityCache::DELETED == value
33
+ result = value
34
+ break
35
+ end
36
+ result = yield
37
+ end
38
+ unless yielded
39
+ result = yield
40
+ add(key, result)
41
+ end
42
+ result
43
+ end
44
+
45
+ private
46
+
47
+ def cas_multi(keys)
48
+ result = nil
49
+ @cache_backend.cas_multi(*keys) do |results|
50
+ deleted = results.select {|_, v| IdentityCache::DELETED == v }
51
+ results.reject! {|_, v| IdentityCache::DELETED == v }
52
+
53
+ result = results
54
+ updates = {}
55
+ missed_keys = keys - results.keys
56
+ unless missed_keys.empty?
57
+ missed_vals = yield missed_keys
58
+ missed_keys.zip(missed_vals) do |k, v|
59
+ result[k] = v
60
+ if deleted.include?(k)
61
+ updates[k] = v
62
+ else
63
+ add(k, v)
64
+ end
65
+ end
66
+ end
67
+
68
+ break if updates.empty?
69
+ updates
70
+ end
71
+ result
72
+ end
73
+
74
+ def add_multi(keys)
75
+ values = yield keys
76
+ result = Hash[keys.zip(values)]
77
+ result.each {|k, v| add(k, v) }
78
+ end
79
+
80
+ def add(key, value)
81
+ @cache_backend.write(key, value, :unless_exist => true)
82
+ end
83
+ end
84
+ end
@@ -2,13 +2,17 @@ require 'monitor'
2
2
 
3
3
  module IdentityCache
4
4
  class MemoizedCacheProxy
5
- attr_accessor :cache_backend
5
+ attr_reader :cache_fetcher
6
6
 
7
- def initialize(cache_backend = nil)
8
- @cache_backend = cache_backend || Rails.cache
7
+ def initialize(cache_adaptor = nil)
8
+ @cache_fetcher = CacheFetcher.new(cache_adaptor)
9
9
  @key_value_maps = Hash.new {|h, k| h[k] = {} }
10
10
  end
11
11
 
12
+ def cache_backend=(cache_adaptor)
13
+ @cache_fetcher.cache_backend = cache_adaptor
14
+ end
15
+
12
16
  def memoized_key_values
13
17
  @key_value_maps[Thread.current]
14
18
  end
@@ -23,52 +27,52 @@ module IdentityCache
23
27
 
24
28
  def write(key, value)
25
29
  memoized_key_values[key] = value if memoizing?
26
- @cache_backend.write(key, value)
30
+ @cache_fetcher.write(key, value)
27
31
  end
28
32
 
29
- def read(key)
30
- used_cache_backend = true
33
+ def delete(key)
34
+ memoized_key_values.delete(key) if memoizing?
35
+ result = @cache_fetcher.delete(key)
36
+ IdentityCache.logger.debug { "[IdentityCache] delete #{ result ? 'recorded' : 'failed' } for #{key}" }
37
+ result
38
+ end
31
39
 
32
- result = if memoizing?
40
+ def fetch(key)
41
+ used_cache_backend = true
42
+ missed = false
43
+ value = if memoizing?
33
44
  used_cache_backend = false
34
- mkv = memoized_key_values
35
-
36
- mkv.fetch(key) do
45
+ memoized_key_values.fetch(key) do
37
46
  used_cache_backend = true
38
- mkv[key] = @cache_backend.read(key)
47
+ memoized_key_values[key] = @cache_fetcher.fetch(key) do
48
+ missed = true
49
+ yield
50
+ end
39
51
  end
40
-
41
52
  else
42
- @cache_backend.read(key)
53
+ @cache_fetcher.fetch(key) do
54
+ missed = true
55
+ yield
56
+ end
43
57
  end
44
58
 
45
- if result
46
- IdentityCache.logger.debug { "[IdentityCache] #{ used_cache_backend ? '(cache_backend)' : '(memoized)' } cache hit for #{key}" }
47
- else
59
+ if missed
48
60
  IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
61
+ else
62
+ IdentityCache.logger.debug { "[IdentityCache] #{ used_cache_backend ? '(cache_backend)' : '(memoized)' } cache hit for #{key}" }
49
63
  end
50
64
 
51
- result
65
+ value
52
66
  end
53
67
 
54
- def delete(key)
55
- memoized_key_values.delete(key) if memoizing?
56
- result = @cache_backend.delete(key)
57
- IdentityCache.logger.debug { "[IdentityCache] delete #{ result ? 'hit' : 'miss' } for #{key}" }
58
- result
59
- end
60
-
61
- def read_multi(*keys)
62
-
63
- if IdentityCache.logger.debug?
64
- memoized_keys , cache_backend_keys = [], []
65
- end
68
+ def fetch_multi(*keys)
69
+ memoized_keys, missed_keys = [], [] if IdentityCache.logger.debug?
66
70
 
67
71
  result = if memoizing?
68
72
  hash = {}
69
73
  mkv = memoized_key_values
70
74
 
71
- missing_keys = keys.reject do |key|
75
+ non_memoized_keys = keys.reject do |key|
72
76
  if mkv.has_key?(key)
73
77
  memoized_keys << key if IdentityCache.logger.debug?
74
78
  hit = mkv[key]
@@ -77,34 +81,30 @@ module IdentityCache
77
81
  end
78
82
  end
79
83
 
80
- hits = missing_keys.empty? ? {} : @cache_backend.read_multi(*missing_keys)
81
-
82
- missing_keys.each do |key|
83
- hit = hits[key]
84
- mkv[key] = hit
85
- hash[key] = hit unless hit.nil?
84
+ unless non_memoized_keys.empty?
85
+ results = @cache_fetcher.fetch_multi(non_memoized_keys) do |missing_keys|
86
+ missed_keys.concat(missing_keys) if IdentityCache.logger.debug?
87
+ yield missing_keys
88
+ end
89
+ mkv.merge! results
90
+ hash.merge! results
86
91
  end
87
92
  hash
88
93
  else
89
- @cache_backend.read_multi(*keys)
90
- end
91
-
92
- if IdentityCache.logger.debug?
93
-
94
- result.each do |k, v|
95
- cache_backend_keys << k if !v.nil? && !memoized_keys.include?(k)
94
+ @cache_fetcher.fetch_multi(keys) do |missing_keys|
95
+ missed_keys.concat(missing_keys) if IdentityCache.logger.debug?
96
+ yield missing_keys
96
97
  end
97
-
98
- memoized_keys.each{ |k| IdentityCache.logger.debug "[IdentityCache] (memoized) cache hit for #{k} (multi)" }
99
- cache_backend_keys.each{ |k| IdentityCache.logger.debug "[IdentityCache] (cache_backend) cache hit for #{k} (multi)" }
100
98
  end
101
99
 
100
+ log_multi_result(memoized_keys, keys - missed_keys - memoized_keys, missed_keys) if IdentityCache.logger.debug?
101
+
102
102
  result
103
103
  end
104
104
 
105
105
  def clear
106
106
  clear_memoization
107
- @cache_backend.clear
107
+ @cache_fetcher.clear
108
108
  end
109
109
 
110
110
  private
@@ -116,5 +116,11 @@ module IdentityCache
116
116
  def memoizing?
117
117
  Thread.current[:memoizing_idc]
118
118
  end
119
+
120
+ def log_multi_result(memoized_keys, backend_keys, missed_keys)
121
+ memoized_keys.each {|k| IdentityCache.logger.debug "[IdentityCache] (memoized) cache hit for #{k} (multi)" }
122
+ backend_keys.each {|k| IdentityCache.logger.debug "[IdentityCache] (backend) cache hit for #{k} (multi)" }
123
+ missed_keys.each {|k| IdentityCache.logger.debug "[IdentityCache] cache miss for #{k} (multi)" }
124
+ end
119
125
  end
120
126
  end
@@ -84,22 +84,7 @@ module IdentityCache
84
84
  coder = coder.dup
85
85
  coder['attributes'] = coder['attributes'].dup
86
86
  end
87
- if record.class._initialize_callbacks.empty?
88
- record.instance_eval do
89
- @attributes = self.class.initialize_attributes(coder['attributes'])
90
- @relation = nil
91
-
92
- @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
93
- @association_cache = {}
94
- @aggregation_cache = {}
95
- @_start_transaction_state = {}
96
- @readonly = @destroyed = @marked_for_destruction = false
97
- @new_record = false
98
- @column_types = self.class.column_types if self.class.respond_to?(:column_types)
99
- end
100
- else
101
- record.init_with(coder)
102
- end
87
+ record.init_with(coder)
103
88
 
104
89
  coder[:associations].each {|name, value| set_embedded_association(record, name, value) } if coder.has_key?(:associations)
105
90
  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)
@@ -1,4 +1,4 @@
1
1
  module IdentityCache
2
- VERSION = "0.1.0"
3
- CACHE_VERSION = 3
2
+ VERSION = "0.2.0"
3
+ CACHE_VERSION = 4
4
4
  end
@@ -11,10 +11,13 @@ require 'identity_cache/parent_model_expiration'
11
11
  require 'identity_cache/query_api'
12
12
  require "identity_cache/cache_hash"
13
13
  require "identity_cache/cache_invalidation"
14
+ require "identity_cache/cache_fetcher"
14
15
 
15
16
  module IdentityCache
16
17
  CACHED_NIL = :idc_cached_nil
17
18
  BATCH_SIZE = 1000
19
+ DELETED = :idc_cached_deleted
20
+ DELETED_TTL = 1000
18
21
 
19
22
  class AlreadyIncludedError < StandardError; end
20
23
  class InverseAssociationError < StandardError
@@ -77,20 +80,11 @@ module IdentityCache
77
80
  # +key+ A cache key string
78
81
  #
79
82
  def fetch(key)
80
- result = cache.read(key) if should_cache?
81
-
82
- if result.nil?
83
- if block_given?
84
- result = yield
85
- result = map_cached_nil_for(result)
86
-
87
- if should_cache?
88
- cache.write(key, result)
89
- end
90
- end
83
+ if should_cache?
84
+ unmap_cached_nil_for(cache.fetch(key) { map_cached_nil_for yield })
85
+ else
86
+ yield
91
87
  end
92
-
93
- unmap_cached_nil_for(result)
94
88
  end
95
89
 
96
90
  def map_cached_nil_for(value)
@@ -109,28 +103,17 @@ module IdentityCache
109
103
  def fetch_multi(*keys)
110
104
  keys.flatten!(1)
111
105
  return {} if keys.size == 0
112
- result = {}
113
- result = read_in_batches(keys) if should_cache?
114
-
115
- hit_keys = result.reject {|key, value| value == nil }.keys
116
- missed_keys = keys - hit_keys
117
-
118
- if missed_keys.size > 0
119
- if block_given?
120
- replacement_results = nil
121
- replacement_results = yield missed_keys
122
- missed_keys.zip(replacement_results) do |(key, replacement_result)|
123
- if should_cache?
124
- replacement_result = map_cached_nil_for(replacement_result )
125
- cache.write(key, replacement_result)
126
- logger.debug { "[IdentityCache] cache miss for #{key} (multi)" }
127
- end
128
- result[key] = replacement_result
129
- end
106
+
107
+ result = if should_cache?
108
+ fetch_in_batches(keys) do |missed_keys|
109
+ results = yield missed_keys
110
+ results.map {|e| map_cached_nil_for e }
130
111
  end
112
+ else
113
+ results = yield keys
114
+ Hash[keys.zip(results)]
131
115
  end
132
116
 
133
-
134
117
  result.each do |key, value|
135
118
  result[key] = unmap_cached_nil_for(value)
136
119
  end
@@ -140,9 +123,9 @@ module IdentityCache
140
123
 
141
124
  private
142
125
 
143
- def read_in_batches(keys)
126
+ def fetch_in_batches(keys)
144
127
  keys.each_slice(BATCH_SIZE).each_with_object Hash.new do |slice, result|
145
- result.merge! cache.read_multi(*slice)
128
+ result.merge! cache.fetch_multi(*slice) {|missed_keys| yield missed_keys }
146
129
  end
147
130
  end
148
131
  end
@@ -6,19 +6,14 @@ require 'identity_cache'
6
6
  require 'memcached_store'
7
7
  require 'active_support/cache/memcached_store'
8
8
 
9
- if ENV['BOXEN_HOME'].present?
10
- $memcached_port = 21211
11
- $mysql_port = 13306
12
- else
13
- $memcached_port = 11211
14
- $mysql_port = 3306
15
- end
9
+ $memcached_port = 11211
10
+ $mysql_port = 3306
16
11
 
17
12
  require File.dirname(__FILE__) + '/../test/helpers/active_record_objects'
18
13
  require File.dirname(__FILE__) + '/../test/helpers/database_connection'
19
14
 
20
15
  IdentityCache.logger = Logger.new(nil)
21
- IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost:#{$memcached_port}")
16
+ IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost:#{$memcached_port}", :support_cas => true)
22
17
 
23
18
 
24
19
  def create_record(id)
@@ -108,6 +103,35 @@ module HitRunner
108
103
  end
109
104
  end
110
105
 
106
+ module DeletedRunner
107
+ def prepare
108
+ super
109
+ (1..@count).each {|i| ::Item.find(i).send(:expire_cache) }
110
+ end
111
+ end
112
+
113
+ module ConflictRunner
114
+ def prepare
115
+ super
116
+ records = (1..@count).map {|id| ::Item.find(id) }
117
+ orig_resolve_cache_miss = ::Item.method(:resolve_cache_miss)
118
+
119
+ ::Item.define_singleton_method(:resolve_cache_miss) do |id|
120
+ records[id-1].send(:expire_cache)
121
+ orig_resolve_cache_miss.call(id)
122
+ end
123
+ IdentityCache.cache.clear
124
+ end
125
+ end
126
+
127
+ module DeletedConflictRunner
128
+ include ConflictRunner
129
+ def prepare
130
+ super
131
+ (1..@count).each {|i| ::Item.find(i).send(:expire_cache) }
132
+ end
133
+ end
134
+
111
135
  class EmbedRunner < CacheRunner
112
136
  def setup_models
113
137
  super
@@ -135,6 +159,20 @@ class FetchEmbedHitRunner < EmbedRunner
135
159
  end
136
160
  CACHE_RUNNERS << FetchEmbedHitRunner
137
161
 
162
+ class FetchEmbedDeletedRunner < EmbedRunner
163
+ include DeletedRunner
164
+ end
165
+ CACHE_RUNNERS << FetchEmbedDeletedRunner
166
+
167
+ class FetchEmbedConflictRunner < EmbedRunner
168
+ include ConflictRunner
169
+ end
170
+ CACHE_RUNNERS << FetchEmbedConflictRunner
171
+
172
+ class FetchEmbedDeletedConflictRunner < EmbedRunner
173
+ include DeletedConflictRunner
174
+ end
175
+ CACHE_RUNNERS << FetchEmbedDeletedConflictRunner
138
176
 
139
177
  class NormalizedRunner < CacheRunner
140
178
  def setup_models
@@ -164,3 +202,18 @@ class FetchNormalizedHitRunner < NormalizedRunner
164
202
  include HitRunner
165
203
  end
166
204
  CACHE_RUNNERS << FetchNormalizedHitRunner
205
+
206
+ class FetchNormalizedDeletedRunner < NormalizedRunner
207
+ include DeletedRunner
208
+ end
209
+ CACHE_RUNNERS << FetchNormalizedDeletedRunner
210
+
211
+ class FetchNormalizedConflictRunner < EmbedRunner
212
+ include ConflictRunner
213
+ end
214
+ CACHE_RUNNERS << FetchNormalizedConflictRunner
215
+
216
+ class FetchNormalizedDeletedConflictRunner < EmbedRunner
217
+ include DeletedConflictRunner
218
+ end
219
+ CACHE_RUNNERS << FetchNormalizedDeletedConflictRunner
@@ -0,0 +1 @@
1
+ # using the default shipit config
@@ -11,25 +11,27 @@ class AttributeCacheTest < IdentityCache::TestCase
11
11
  @record = @parent.associated_records.create!(:name => 'foo')
12
12
  @name_attribute_key = "#{NAMESPACE}attribute:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s)}"
13
13
  @blob_key = "#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,item_id:integer,name:string")}:1"
14
+ IdentityCache.cache.clear
14
15
  end
15
16
 
16
17
  def test_attribute_values_are_returned_on_cache_hits
17
- IdentityCache.cache.expects(:read).with(@name_attribute_key).returns('foo')
18
+ IdentityCache.cache.expects(:fetch).with(@name_attribute_key).returns('foo')
18
19
  assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
19
20
  end
20
21
 
21
22
  def test_attribute_values_are_fetched_and_returned_on_cache_misses
22
- IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
23
+ fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
23
24
  Item.connection.expects(:exec_query)
24
25
  .with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
25
26
  .returns(ActiveRecord::Result.new(['name'], [['foo']]))
26
27
 
27
28
  assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
29
+ assert fetch.has_been_called_with?(@name_attribute_key)
28
30
  end
29
31
 
30
32
  def test_attribute_values_are_stored_in_the_cache_on_cache_misses
31
33
  # Cache miss, so
32
- IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
34
+ fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
33
35
 
34
36
  # Grab the value of the attribute from the DB
35
37
  Item.connection.expects(:exec_query)
@@ -37,14 +39,17 @@ class AttributeCacheTest < IdentityCache::TestCase
37
39
  .returns(ActiveRecord::Result.new(['name'], [['foo']]))
38
40
 
39
41
  # And write it back to the cache
40
- IdentityCache.cache.expects(:write).with(@name_attribute_key, 'foo').returns(nil)
42
+ add = Spy.on(fetcher, :add).and_call_through
41
43
 
42
44
  assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
45
+ assert fetch.has_been_called_with?(@name_attribute_key)
46
+ assert add.has_been_called_with?(@name_attribute_key, 'foo')
47
+ assert_equal 'foo', IdentityCache.cache.fetch(@name_attribute_key)
43
48
  end
44
49
 
45
50
  def test_nil_is_stored_in_the_cache_on_cache_misses
46
51
  # Cache miss, so
47
- IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
52
+ fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
48
53
 
49
54
  # Grab the value of the attribute from the DB
50
55
  Item.connection.expects(:exec_query)
@@ -52,9 +57,11 @@ class AttributeCacheTest < IdentityCache::TestCase
52
57
  .returns(ActiveRecord::Result.new(['name'], []))
53
58
 
54
59
  # And write it back to the cache
55
- IdentityCache.cache.expects(:write).with(@name_attribute_key, IdentityCache::CACHED_NIL).returns(nil)
60
+ add = Spy.on(fetcher, :add).and_call_through
56
61
 
57
62
  assert_equal nil, AssociatedRecord.fetch_name_by_id(1)
63
+ assert fetch.has_been_called_with?(@name_attribute_key)
64
+ assert add.has_been_called_with?(@name_attribute_key, IdentityCache::CACHED_NIL)
58
65
  end
59
66
 
60
67
  def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_saved
@@ -44,7 +44,7 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
44
44
  def test_changes_in_associated_records_should_expire_the_parents_cache
45
45
  Item.fetch(@record.id)
46
46
  key = @record.primary_cache_index_key
47
- assert_not_nil IdentityCache.cache.read(key)
47
+ assert_not_nil IdentityCache.cache.fetch(key)
48
48
 
49
49
  IdentityCache.cache.expects(:delete).with(@record.associated_records.first.primary_cache_index_key)
50
50
  IdentityCache.cache.expects(:delete).with(key)
@@ -13,14 +13,15 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
13
13
  end
14
14
 
15
15
  def test_on_cache_miss_record_should_embed_associated_object
16
- IdentityCache.cache.expects(:read).with(@record.secondary_cache_index_key_for_current_values([:title]))
17
- IdentityCache.cache.expects(:read).with(@record.primary_cache_index_key)
16
+ fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
18
17
 
19
18
  record_from_cache_miss = Item.fetch_by_title('foo')
20
19
 
21
20
  assert_equal @record, record_from_cache_miss
22
21
  assert_not_nil @record.fetch_associated
23
22
  assert_equal @record.associated, record_from_cache_miss.fetch_associated
23
+ assert fetch.has_been_called_with?(@record.secondary_cache_index_key_for_current_values([:title]))
24
+ assert fetch.has_been_called_with?(@record.primary_cache_index_key)
24
25
  end
25
26
 
26
27
  def test_on_cache_miss_record_should_embed_nil_object
@@ -31,8 +32,7 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
31
32
  @record.send(:populate_association_caches)
32
33
  Item.expects(:resolve_cache_miss).with(@record.id).once.returns(@record)
33
34
 
34
- IdentityCache.cache.expects(:read).with(@record.secondary_cache_index_key_for_current_values([:title]))
35
- IdentityCache.cache.expects(:read).with(@record.primary_cache_index_key)
35
+ fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
36
36
 
37
37
  record_from_cache_miss = Item.fetch_by_title('foo')
38
38
  record_from_cache_miss.expects(:associated).never
@@ -41,6 +41,8 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
41
41
  5.times do
42
42
  assert_nil record_from_cache_miss.fetch_associated
43
43
  end
44
+ assert fetch.has_been_called_with?(@record.secondary_cache_index_key_for_current_values([:title]))
45
+ assert fetch.has_been_called_with?(@record.primary_cache_index_key)
44
46
  end
45
47
 
46
48
  def test_on_record_from_the_db_will_use_normal_association
@@ -83,7 +85,7 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
83
85
  def test_changes_in_associated_record_should_expire_the_parents_cache
84
86
  Item.fetch_by_title('foo')
85
87
  key = @record.primary_cache_index_key
86
- assert_not_nil IdentityCache.cache.read(key)
88
+ assert_not_nil IdentityCache.cache.fetch(key)
87
89
 
88
90
  IdentityCache.cache.expects(:delete).at_least(1).with(key)
89
91
  IdentityCache.cache.expects(:delete).with(@record.associated.primary_cache_index_key)