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 +4 -4
- checksums.yaml.gz.sig +1 -0
- data/.travis.yml +0 -4
- data/README.md +5 -1
- data/identity_cache.gemspec +5 -5
- data/lib/identity_cache/cache_fetcher.rb +84 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +53 -47
- data/lib/identity_cache/query_api.rb +1 -16
- data/lib/identity_cache/version.rb +2 -2
- data/lib/identity_cache.rb +17 -34
- data/performance/cache_runner.rb +61 -8
- data/shipit.rubygems.yml +1 -0
- data/test/attribute_cache_test.rb +13 -6
- data/test/denormalized_has_many_test.rb +1 -1
- data/test/denormalized_has_one_test.rb +7 -5
- data/test/fetch_multi_test.rb +39 -15
- data/test/fetch_test.rb +66 -26
- data/test/fixtures/serialized_record +0 -0
- data/test/helpers/update_serialization_format.rb +6 -8
- data/test/index_cache_test.rb +13 -13
- data/test/memoized_cache_proxy_test.rb +29 -33
- data/test/test_helper.rb +7 -10
- data.tar.gz.sig +1 -0
- metadata +59 -6
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46228f11af3be0212c16960de9cb1d321ceac8db
|
4
|
+
data.tar.gz: bf0fca38334faff8e71ea229bf472570efc969ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
250
|
+
Francis Bogsanyi (@fbogsany)
|
247
251
|
Arthur Neves (@arthurnn)
|
data/identity_cache.gemspec
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
5
|
+
attr_reader :cache_fetcher
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
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
|
-
@
|
30
|
+
@cache_fetcher.write(key, value)
|
27
31
|
end
|
28
32
|
|
29
|
-
def
|
30
|
-
|
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
|
-
|
40
|
+
def fetch(key)
|
41
|
+
used_cache_backend = true
|
42
|
+
missed = false
|
43
|
+
value = if memoizing?
|
33
44
|
used_cache_backend = false
|
34
|
-
|
35
|
-
|
36
|
-
mkv.fetch(key) do
|
45
|
+
memoized_key_values.fetch(key) do
|
37
46
|
used_cache_backend = true
|
38
|
-
|
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
|
-
@
|
53
|
+
@cache_fetcher.fetch(key) do
|
54
|
+
missed = true
|
55
|
+
yield
|
56
|
+
end
|
43
57
|
end
|
44
58
|
|
45
|
-
if
|
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
|
-
|
65
|
+
value
|
52
66
|
end
|
53
67
|
|
54
|
-
def
|
55
|
-
|
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
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
@
|
90
|
-
|
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
|
-
@
|
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
|
-
|
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)
|
data/lib/identity_cache.rb
CHANGED
@@ -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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
113
|
-
result =
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
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.
|
128
|
+
result.merge! cache.fetch_multi(*slice) {|missed_keys| yield missed_keys }
|
146
129
|
end
|
147
130
|
end
|
148
131
|
end
|
data/performance/cache_runner.rb
CHANGED
@@ -6,19 +6,14 @@ require 'identity_cache'
|
|
6
6
|
require 'memcached_store'
|
7
7
|
require 'active_support/cache/memcached_store'
|
8
8
|
|
9
|
-
|
10
|
-
|
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
|
data/shipit.rubygems.yml
ADDED
@@ -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(:
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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.
|
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
|
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
|
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.
|
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)
|