identity_cache 0.2.2 → 0.2.3
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 +0 -0
- data.tar.gz.sig +0 -0
- data/.travis.yml +18 -2
- data/CHANGELOG.md +67 -0
- data/CONTRIBUTING.md +35 -0
- data/Gemfile +0 -5
- data/{Gemfile32 → Gemfile.rails32} +0 -2
- data/Gemfile.rails40 +5 -0
- data/Gemfile.rails41 +1 -3
- data/Gemfile.rails42 +5 -0
- data/README.md +11 -38
- data/identity_cache.gemspec +1 -0
- data/lib/identity_cache.rb +4 -3
- data/lib/identity_cache/cache_fetcher.rb +6 -6
- data/lib/identity_cache/configuration_dsl.rb +8 -5
- data/lib/identity_cache/fallback_fetcher.rb +5 -5
- data/lib/identity_cache/parent_model_expiration.rb +23 -2
- data/lib/identity_cache/query_api.rb +1 -1
- data/lib/identity_cache/version.rb +1 -1
- data/test/attribute_cache_test.rb +26 -17
- data/test/cache_invalidation_test.rb +72 -1
- data/test/denormalized_has_many_test.rb +1 -1
- data/test/denormalized_has_one_test.rb +1 -1
- data/test/fetch_test.rb +12 -1
- data/test/fixtures/serialized_record +0 -0
- data/test/helpers/active_record_objects.rb +8 -0
- data/test/helpers/database_connection.rb +24 -13
- data/test/helpers/update_serialization_format.rb +1 -4
- data/test/identity_cache_test.rb +9 -0
- data/test/index_cache_test.rb +3 -3
- data/test/readonly_test.rb +16 -11
- data/test/save_test.rb +33 -17
- data/test/test_helper.rb +8 -6
- metadata +21 -4
- metadata.gz.sig +0 -0
- data/CHANGELOG +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ae6ab233ca71a68a76c92e46a6819af91b02c39
|
4
|
+
data.tar.gz: 1bf83f15e3957a5b20133cdad5cc80d675496973
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45ad03a05a496eccb4327ae9ba775174cbf520a25c0087119fcde8de95ec7e77be79b0ab2dd65fcb99ad588af18d8b1992f126551e04daf58bca28d5a0f5c466
|
7
|
+
data.tar.gz: 6e9ecd4f00f97adf917e0ad354cb3cadba716b44dd75fd27d95c9437f564ea70af73312841bb89291b0a4beb5310d87538664b5930e2f572566013ac67b04e25
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/.travis.yml
CHANGED
@@ -1,14 +1,30 @@
|
|
1
1
|
language: ruby
|
2
|
+
|
2
3
|
rvm:
|
3
4
|
- 1.9.3
|
4
5
|
- 2.0.0
|
5
6
|
- 2.1.1
|
7
|
+
|
6
8
|
gemfile:
|
7
|
-
-
|
8
|
-
- Gemfile
|
9
|
+
- Gemfile.rails32
|
10
|
+
- Gemfile.rails40
|
9
11
|
- Gemfile.rails41
|
12
|
+
- Gemfile.rails42
|
13
|
+
|
14
|
+
env:
|
15
|
+
- DB=mysql2
|
16
|
+
- DB=postgresql
|
17
|
+
|
10
18
|
services:
|
11
19
|
- memcache
|
12
20
|
- mysql
|
21
|
+
|
22
|
+
sudo: false
|
23
|
+
|
13
24
|
before_script:
|
14
25
|
- mysql -e 'create database identity_cache_test'
|
26
|
+
- psql -c 'create database identity_cache_test;' -U postgres
|
27
|
+
|
28
|
+
matrix:
|
29
|
+
allow_failures:
|
30
|
+
- gemfile: Gemfile.rails42
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# IdentityCache changelog
|
2
|
+
|
3
|
+
#### Unreleased
|
4
|
+
|
5
|
+
- PostgreSQL support
|
6
|
+
- Rails 4.2 compatibility
|
7
|
+
- Fix: Don't connect to database when calling `IdentityCache.should_use_cache?`
|
8
|
+
- Fix: Fix invalid parent cache invalidation if object is embedded in different parents
|
9
|
+
|
10
|
+
#### 0.2.2
|
11
|
+
|
12
|
+
- Change: memcached is no longer a runtime dependency
|
13
|
+
- Use cache for read-only models.
|
14
|
+
|
15
|
+
#### 0.2.1
|
16
|
+
|
17
|
+
- Add a fallback backend using local memory.
|
18
|
+
|
19
|
+
#### 0.2.0
|
20
|
+
|
21
|
+
- Memcache CAS support
|
22
|
+
|
23
|
+
#### 0.1.0
|
24
|
+
|
25
|
+
- Backwards incompatible change: Stop expiring cache on after_touch callback.
|
26
|
+
- Change: fetch_multi accepts an array of keys as argument
|
27
|
+
- Change: :embed option value from false to :ids for cache_has_many for clarity
|
28
|
+
- Fix: Consistently use ActiveRecord / Arel APIs to build SQL queries
|
29
|
+
- Fix: `SystemStackError` when fetching more records than the max stack size
|
30
|
+
- Fix: Bug in `fetch_multi` in a transaction where results weren't compacted.
|
31
|
+
- Fix: Avoid unused preload on fetch_multi with :includes option for cache miss
|
32
|
+
- Fix: reload will invalidate the local instance cache
|
33
|
+
|
34
|
+
#### 0.0.7
|
35
|
+
|
36
|
+
- Add support for non-integer primary keys
|
37
|
+
- Fix: Not implemented error for cache_has_one with embed: false
|
38
|
+
- Fix: cache key to change when adding a cache_has_many association with :embed => false
|
39
|
+
- Fix: Compatibility rails 4.1 for `quote_value`, which needs default column.
|
40
|
+
|
41
|
+
#### 0.0.6
|
42
|
+
|
43
|
+
- Fix: bug where previously nil-cached attribute caches weren't expired on record creation
|
44
|
+
- Fix: cache key to not change when adding a non-embedded association.
|
45
|
+
- Perf: Rails 4 Only create `CollectionProxy` when using it
|
46
|
+
|
47
|
+
#### 0.0.5
|
48
|
+
|
49
|
+
|
50
|
+
#### 0.0.4
|
51
|
+
|
52
|
+
- Fix: only marshal attributes, embedded associations and normalized association IDs
|
53
|
+
- Add cache version number to cache keys
|
54
|
+
- Add test case to ensure version number is updated when the marshalled format changes
|
55
|
+
|
56
|
+
#### 0.0.3
|
57
|
+
|
58
|
+
- Fix: memoization for multi hits actually work
|
59
|
+
- Fix: quotes `SELECT` projection elements on cache misses
|
60
|
+
- Add CPU performance benchmark
|
61
|
+
- Fix: table names are not hardcoded anymore
|
62
|
+
- Logger now differentiates memoized vs non memoized hits
|
63
|
+
|
64
|
+
#### 0.0.2
|
65
|
+
|
66
|
+
- Fix: Existent embedded entries will no longer raise when `ActiveModel::MissingAttributeError` when accessing a newly created attribute.
|
67
|
+
- Fix: Do not marshal raw ActiveRecord associations
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
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.
|
4
|
+
|
5
|
+
Types of contributions we welcome:
|
6
|
+
|
7
|
+
- Bug fixes
|
8
|
+
- Performance improvements
|
9
|
+
- Documentation and/or clearer interfaces
|
10
|
+
|
11
|
+
|
12
|
+
### How To Contribute
|
13
|
+
|
14
|
+
1. Fork it
|
15
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
16
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
17
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
18
|
+
5. Create new Pull Request
|
19
|
+
|
20
|
+
Please keep the following in mind:
|
21
|
+
|
22
|
+
- Add a short entry to the "unreleased" section in [CHANGELOG.md](./CHANGELOG.md) describing your changes.
|
23
|
+
- Do not change `IdentityCache::VERSION`; this is done as part of the release process.
|
24
|
+
|
25
|
+
|
26
|
+
## Contributors
|
27
|
+
|
28
|
+
- Camilo Lopez (@camilo)
|
29
|
+
- Tom Burns (@boourns)
|
30
|
+
- Harry Brundage (@hornairs)
|
31
|
+
- Dylan Smith (@dylanahsmith)
|
32
|
+
- Tobias Lütke (@tobi)
|
33
|
+
- John Duff (@jduff)
|
34
|
+
- Francis Bogsanyi (@fbogsany)
|
35
|
+
- Arthur Neves (@arthurnn)
|
data/Gemfile
CHANGED
data/Gemfile.rails40
ADDED
data/Gemfile.rails41
CHANGED
data/Gemfile.rails42
ADDED
data/README.md
CHANGED
@@ -157,25 +157,25 @@ This will read the attribute from the cache or query the database for the attrib
|
|
157
157
|
|
158
158
|
#### cache_index
|
159
159
|
|
160
|
-
Options:
|
160
|
+
Options:
|
161
161
|
_[:unique]_ Allows you to say that an index is unique (only one object stored at the index) or not unique, which allows there to be multiple objects matching the index key. The default value is false.
|
162
162
|
|
163
|
-
Example:
|
163
|
+
Example:
|
164
164
|
`cache_index :handle`
|
165
165
|
|
166
166
|
#### cache_has_many
|
167
167
|
|
168
|
-
Options:
|
168
|
+
Options:
|
169
169
|
_[:embed]_ When true, specifies that the association should be included with the parent when caching. This means the associated objects will be loaded already when the parent is loaded from the cache and will not need to be fetched on their own. When :ids, only the id of the associated records will be included with the parent when caching.
|
170
170
|
|
171
171
|
_[:inverse_name]_ Specifies the name of parent object used by the association. This is useful for polymorphic associations when the association is often named something different between the parent and child objects.
|
172
172
|
|
173
|
-
Example:
|
173
|
+
Example:
|
174
174
|
`cache_has_many :metafields, :inverse_name => :owner, :embed => true`
|
175
175
|
|
176
176
|
#### cache_has_one
|
177
177
|
|
178
|
-
Options:
|
178
|
+
Options:
|
179
179
|
_[:embed]_ When true, specifies that the association should be included with the parent when caching. This means the associated objects will be loaded already when the parent is loaded from the cache and will not need to be fetched on their own. No other values are currently implemented.
|
180
180
|
|
181
181
|
_[:inverse_name]_ Specifies the name of parent object used by the association. This is useful for polymorphic associations when the association is often named something different between the parent and child objects.
|
@@ -185,10 +185,10 @@ Example:
|
|
185
185
|
|
186
186
|
#### cache_attribute
|
187
187
|
|
188
|
-
Options:
|
188
|
+
Options:
|
189
189
|
_[:by]_ Specifies what key(s) you want the attribute cached by. Defaults to :id.
|
190
190
|
|
191
|
-
Example:
|
191
|
+
Example:
|
192
192
|
`cache_attribute :target, :by => [:shop_id, :path]`
|
193
193
|
|
194
194
|
## Memoized Cache Proxy
|
@@ -217,35 +217,8 @@ 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
|
-
##
|
221
|
-
|
222
|
-
JRuby will not work with this current version, as we are using the memcached gem internally to interface with memcache.
|
223
|
-
|
224
|
-
## Contributing
|
225
|
-
|
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.
|
227
|
-
|
228
|
-
Types of contributions we are looking for:
|
229
|
-
|
230
|
-
- Bug fixes
|
231
|
-
- Performance improvements
|
232
|
-
- Documentation and/or clearer interfaces
|
233
|
-
|
234
|
-
### How To Contribute
|
235
|
-
|
236
|
-
1. Fork it
|
237
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
238
|
-
3. Commit your changes (`git commit -am 'Added some feature'`)
|
239
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
240
|
-
5. Create new Pull Request
|
241
|
-
|
242
|
-
## Contributors
|
220
|
+
## Notes
|
243
221
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
Dylan Smith (@dylanahsmith)
|
248
|
-
Tobias Lütke (@tobi)
|
249
|
-
John Duff (@jduff)
|
250
|
-
Francis Bogsanyi (@fbogsany)
|
251
|
-
Arthur Neves (@arthurnn)
|
222
|
+
- JRuby will not work with this current version, as we are using the memcached gem internally to interface with memcache.
|
223
|
+
- See CHANGELOG.md for a list of changes to the library over time.
|
224
|
+
- The librray is MIT licensed and we welcome contributions. See CONTRIBUTING.md for more information.
|
data/identity_cache.gemspec
CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |gem|
|
|
30
30
|
else
|
31
31
|
gem.add_development_dependency('cityhash', '0.6.0')
|
32
32
|
gem.add_development_dependency('mysql2')
|
33
|
+
gem.add_development_dependency('pg')
|
33
34
|
gem.add_development_dependency('stackprof') if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0")
|
34
35
|
end
|
35
36
|
end
|
data/lib/identity_cache.rb
CHANGED
@@ -69,12 +69,13 @@ module IdentityCache
|
|
69
69
|
@logger || Rails.logger
|
70
70
|
end
|
71
71
|
|
72
|
-
def
|
73
|
-
!readonly
|
72
|
+
def should_fill_cache? # :nodoc:
|
73
|
+
!readonly
|
74
74
|
end
|
75
75
|
|
76
76
|
def should_use_cache? # :nodoc:
|
77
|
-
ActiveRecord::Base.
|
77
|
+
pool = ActiveRecord::Base.connection_pool
|
78
|
+
!pool.active_connection? || pool.connection.open_transactions == 0
|
78
79
|
end
|
79
80
|
|
80
81
|
# Cache retrieval and miss resolver primitive; given a key it will try to
|
@@ -7,15 +7,15 @@ module IdentityCache
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def write(key, value)
|
10
|
-
@cache_backend.write(key, value) if IdentityCache.
|
10
|
+
@cache_backend.write(key, value) if IdentityCache.should_fill_cache?
|
11
11
|
end
|
12
12
|
|
13
13
|
def delete(key)
|
14
|
-
@cache_backend.write(key, IdentityCache::DELETED, :expires_in => IdentityCache::DELETED_TTL.seconds)
|
14
|
+
@cache_backend.write(key, IdentityCache::DELETED, :expires_in => IdentityCache::DELETED_TTL.seconds)
|
15
15
|
end
|
16
16
|
|
17
17
|
def clear
|
18
|
-
@cache_backend.clear
|
18
|
+
@cache_backend.clear
|
19
19
|
end
|
20
20
|
|
21
21
|
def fetch_multi(keys, &block)
|
@@ -34,7 +34,7 @@ module IdentityCache
|
|
34
34
|
break
|
35
35
|
end
|
36
36
|
result = yield
|
37
|
-
break unless IdentityCache.
|
37
|
+
break unless IdentityCache.should_fill_cache?
|
38
38
|
result
|
39
39
|
end
|
40
40
|
unless yielded
|
@@ -68,7 +68,7 @@ module IdentityCache
|
|
68
68
|
end
|
69
69
|
|
70
70
|
break if updates.empty?
|
71
|
-
break unless IdentityCache.
|
71
|
+
break unless IdentityCache.should_fill_cache?
|
72
72
|
updates
|
73
73
|
end
|
74
74
|
result
|
@@ -81,7 +81,7 @@ module IdentityCache
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def add(key, value)
|
84
|
-
@cache_backend.write(key, value, :unless_exist => true) if IdentityCache.
|
84
|
+
@cache_backend.write(key, value, :unless_exist => true) if IdentityCache.should_fill_cache?
|
85
85
|
end
|
86
86
|
end
|
87
87
|
end
|
@@ -273,17 +273,20 @@ module IdentityCache
|
|
273
273
|
child_association = child_class.reflect_on_association(options[:inverse_name])
|
274
274
|
raise InverseAssociationError unless child_association
|
275
275
|
foreign_key = child_association.association_foreign_key
|
276
|
-
parent_class ||= self.name
|
277
276
|
|
278
277
|
child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
|
279
278
|
child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
|
280
279
|
|
280
|
+
after_action_name = "expire_parent_cache_#{self.name.underscore}"
|
281
|
+
|
281
282
|
child_class.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
|
282
|
-
|
283
|
-
|
283
|
+
|
284
|
+
after_commit :#{after_action_name}
|
285
|
+
after_touch :#{after_action_name}
|
286
|
+
add_parent_expiration_entry :#{after_action_name}
|
284
287
|
|
285
|
-
def
|
286
|
-
expire_parent_cache_on_changes(:#{options[:inverse_name]}, '#{foreign_key}', #{
|
288
|
+
def #{after_action_name}
|
289
|
+
expire_parent_cache_on_changes(:#{options[:inverse_name]}, '#{foreign_key}', #{self.name}, #{options[:only_on_foreign_key_change]})
|
287
290
|
end
|
288
291
|
CODE
|
289
292
|
end
|
@@ -7,15 +7,15 @@ module IdentityCache
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def write(key, value)
|
10
|
-
@cache_backend.write(key, value) if IdentityCache.
|
10
|
+
@cache_backend.write(key, value) if IdentityCache.should_fill_cache?
|
11
11
|
end
|
12
12
|
|
13
13
|
def delete(key)
|
14
|
-
@cache_backend.delete(key)
|
14
|
+
@cache_backend.delete(key)
|
15
15
|
end
|
16
16
|
|
17
17
|
def clear
|
18
|
-
@cache_backend.clear
|
18
|
+
@cache_backend.clear
|
19
19
|
end
|
20
20
|
|
21
21
|
def fetch_multi(keys, &block)
|
@@ -24,7 +24,7 @@ module IdentityCache
|
|
24
24
|
unless missed_keys.empty?
|
25
25
|
replacement_results = yield missed_keys
|
26
26
|
missed_keys.zip(replacement_results) do |key, replacement_result|
|
27
|
-
@cache_backend.write(key, replacement_result) if IdentityCache.
|
27
|
+
@cache_backend.write(key, replacement_result) if IdentityCache.should_fill_cache?
|
28
28
|
results[key] = replacement_result
|
29
29
|
end
|
30
30
|
end
|
@@ -35,7 +35,7 @@ module IdentityCache
|
|
35
35
|
result = @cache_backend.read(key)
|
36
36
|
if result.nil?
|
37
37
|
result = yield
|
38
|
-
@cache_backend.write(key, result) if IdentityCache.
|
38
|
+
@cache_backend.write(key, result) if IdentityCache.should_fill_cache?
|
39
39
|
end
|
40
40
|
result
|
41
41
|
end
|
@@ -1,12 +1,33 @@
|
|
1
1
|
module IdentityCache
|
2
2
|
module ParentModelExpiration # :nodoc:
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do |base|
|
6
|
+
base.class_attribute :parent_expiration_entries
|
7
|
+
base.parent_expiration_entries = Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
private
|
12
|
+
|
13
|
+
def add_parent_expiration_entry(after_action_name)
|
14
|
+
parent_expiration_entries << after_action_name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def expire_parent_caches
|
19
|
+
self.class.parent_expiration_entries.each do |parent_expiration_entry|
|
20
|
+
send(parent_expiration_entry)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
3
24
|
def expire_parent_cache_on_changes(parent_name, foreign_key, parent_class, only_on_foreign_key_change)
|
4
25
|
new_parent = send(parent_name)
|
5
26
|
|
6
27
|
if new_parent && new_parent.respond_to?(:expire_primary_index, true)
|
7
28
|
if should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
|
8
29
|
new_parent.send(:expire_primary_index)
|
9
|
-
new_parent.send(:
|
30
|
+
new_parent.send(:expire_parent_caches) if new_parent.respond_to?(:expire_parent_caches, true)
|
10
31
|
end
|
11
32
|
end
|
12
33
|
|
@@ -14,7 +35,7 @@ module IdentityCache
|
|
14
35
|
begin
|
15
36
|
old_parent = parent_class.find(transaction_changed_attributes[foreign_key])
|
16
37
|
old_parent.send(:expire_primary_index) if old_parent.respond_to?(:expire_primary_index, true)
|
17
|
-
old_parent.send(:
|
38
|
+
old_parent.send(:expire_parent_caches) if old_parent.respond_to?(:expire_parent_caches, true)
|
18
39
|
rescue ActiveRecord::RecordNotFound => e
|
19
40
|
# suppress errors finding the old parent if its been destroyed since it will have expired itself in that case
|
20
41
|
end
|
@@ -204,7 +204,7 @@ module IdentityCache
|
|
204
204
|
|
205
205
|
def find_batch(ids)
|
206
206
|
@id_column ||= columns.detect {|c| c.name == primary_key}
|
207
|
-
ids = ids.map{ |id|
|
207
|
+
ids = ids.map{ |id| connection.type_cast(id, @id_column) }
|
208
208
|
records = where(primary_key => ids).includes(cache_fetch_includes).to_a
|
209
209
|
records_by_id = records.index_by(&:id)
|
210
210
|
ids.map{ |id| records_by_id[id] }
|
@@ -10,7 +10,6 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
10
10
|
@parent = Item.create!(:title => 'bob')
|
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
|
-
@blob_key = "#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,item_id:integer,name:string")}:1"
|
14
13
|
IdentityCache.cache.clear
|
15
14
|
end
|
16
15
|
|
@@ -21,9 +20,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
21
20
|
|
22
21
|
def test_attribute_values_are_fetched_and_returned_on_cache_misses
|
23
22
|
fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
|
24
|
-
|
25
|
-
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
26
|
-
.returns(ActiveRecord::Result.new(['name'], [['foo']]))
|
23
|
+
expects_fetch_associated_record_name_by_id(1, returns: 'foo')
|
27
24
|
|
28
25
|
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
29
26
|
assert fetch.has_been_called_with?(@name_attribute_key)
|
@@ -34,9 +31,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
34
31
|
fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
|
35
32
|
|
36
33
|
# Grab the value of the attribute from the DB
|
37
|
-
|
38
|
-
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
39
|
-
.returns(ActiveRecord::Result.new(['name'], [['foo']]))
|
34
|
+
expects_fetch_associated_record_name_by_id(1, returns: 'foo')
|
40
35
|
|
41
36
|
# And write it back to the cache
|
42
37
|
add = Spy.on(fetcher, :add).and_call_through
|
@@ -52,9 +47,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
52
47
|
fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
|
53
48
|
|
54
49
|
# Grab the value of the attribute from the DB
|
55
|
-
|
56
|
-
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
57
|
-
.returns(ActiveRecord::Result.new(['name'], []))
|
50
|
+
expects_fetch_associated_record_name_by_id(1, returns: nil)
|
58
51
|
|
59
52
|
# And write it back to the cache
|
60
53
|
add = Spy.on(fetcher, :add).and_call_through
|
@@ -66,27 +59,27 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
66
59
|
|
67
60
|
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_saved
|
68
61
|
IdentityCache.cache.expects(:delete).with(@name_attribute_key)
|
69
|
-
IdentityCache.cache.expects(:delete).with(
|
62
|
+
IdentityCache.cache.expects(:delete).with(blob_key_for_associated_record(1))
|
70
63
|
@record.save!
|
71
64
|
end
|
72
65
|
|
73
66
|
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_with_changed_attributes_is_saved
|
74
67
|
IdentityCache.cache.expects(:delete).with(@name_attribute_key)
|
75
|
-
IdentityCache.cache.expects(:delete).with(
|
68
|
+
IdentityCache.cache.expects(:delete).with(blob_key_for_associated_record(1))
|
76
69
|
@record.name = 'bar'
|
77
70
|
@record.save!
|
78
71
|
end
|
79
72
|
|
80
73
|
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_destroyed
|
81
74
|
IdentityCache.cache.expects(:delete).with(@name_attribute_key)
|
82
|
-
IdentityCache.cache.expects(:delete).with(
|
75
|
+
IdentityCache.cache.expects(:delete).with(blob_key_for_associated_record(1))
|
83
76
|
@record.destroy
|
84
77
|
end
|
85
78
|
|
86
79
|
def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
|
87
80
|
new_id = 2.to_s
|
88
81
|
# primary index delete
|
89
|
-
IdentityCache.cache.expects(:delete).with(
|
82
|
+
IdentityCache.cache.expects(:delete).with(blob_key_for_associated_record(new_id))
|
90
83
|
# attribute cache delete
|
91
84
|
IdentityCache.cache.expects(:delete).with("#{NAMESPACE}attribute:AssociatedRecord:name:id:#{cache_hash(new_id)}")
|
92
85
|
@parent.associated_records.create(:name => 'bar')
|
@@ -95,9 +88,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
95
88
|
def test_fetching_by_attribute_delegates_to_block_if_transactions_are_open
|
96
89
|
IdentityCache.cache.expects(:read).with(@name_attribute_key).never
|
97
90
|
|
98
|
-
|
99
|
-
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
100
|
-
.returns(ActiveRecord::Result.new(['name'], [['foo']]))
|
91
|
+
expects_fetch_associated_record_name_by_id(1, returns: 'foo')
|
101
92
|
|
102
93
|
@record.transaction do
|
103
94
|
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
@@ -109,4 +100,22 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
109
100
|
AssociatedRecord.create(:name => "Jim")
|
110
101
|
assert_equal "Jim", AssociatedRecord.fetch_name_by_id(2)
|
111
102
|
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def blob_key_for_associated_record(id)
|
107
|
+
cache_hash = cache_hash('id:integer,item_id:integer,item_two_id:integer,name:string')
|
108
|
+
"#{NAMESPACE}blob:AssociatedRecord:#{cache_hash}:#{id}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def quoted_table_column(model, column_name)
|
112
|
+
"#{model.quoted_table_name}.#{model.connection.quote_column_name(column_name)}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def expects_fetch_associated_record_name_by_id(id, options={})
|
116
|
+
result = options[:returns] ? [options[:returns]] : []
|
117
|
+
Item.connection.expects(:exec_query)
|
118
|
+
.with(AssociatedRecord.unscoped.select(quoted_table_column(AssociatedRecord, :name)).where(id: id).limit(1).to_sql, any_parameters)
|
119
|
+
.returns(ActiveRecord::Result.new(['name'], [result]))
|
120
|
+
end
|
112
121
|
end
|
@@ -3,7 +3,6 @@ require "test_helper"
|
|
3
3
|
class CacheInvalidationTest < IdentityCache::TestCase
|
4
4
|
def setup
|
5
5
|
super
|
6
|
-
Item.cache_has_many :associated_records, :embed => :ids
|
7
6
|
|
8
7
|
@record = Item.new(:title => 'foo')
|
9
8
|
@record.associated_records << AssociatedRecord.new(:name => 'bar')
|
@@ -14,6 +13,8 @@ class CacheInvalidationTest < IdentityCache::TestCase
|
|
14
13
|
end
|
15
14
|
|
16
15
|
def test_reload_invalidate_cached_ids
|
16
|
+
Item.cache_has_many :associated_records, :embed => :ids
|
17
|
+
|
17
18
|
variable_name = "@#{@record.class.send(:embedded_associations)[:associated_records][:ids_variable_name]}"
|
18
19
|
|
19
20
|
@record.fetch_associated_record_ids
|
@@ -27,6 +28,8 @@ class CacheInvalidationTest < IdentityCache::TestCase
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def test_reload_invalidate_cached_objects
|
31
|
+
Item.cache_has_many :associated_records, :embed => :ids
|
32
|
+
|
30
33
|
variable_name = "@#{@record.class.send(:embedded_associations)[:associated_records][:records_variable_name]}"
|
31
34
|
|
32
35
|
@record.fetch_associated_records
|
@@ -40,6 +43,8 @@ class CacheInvalidationTest < IdentityCache::TestCase
|
|
40
43
|
end
|
41
44
|
|
42
45
|
def test_after_a_reload_the_cache_perform_as_expected
|
46
|
+
Item.cache_has_many :associated_records, :embed => :ids
|
47
|
+
|
43
48
|
assert_equal [@baz, @bar], @record.associated_records
|
44
49
|
assert_equal [@baz, @bar], @record.fetch_associated_records
|
45
50
|
|
@@ -49,4 +54,70 @@ class CacheInvalidationTest < IdentityCache::TestCase
|
|
49
54
|
assert_equal [@bar], @record.associated_records
|
50
55
|
assert_equal [@bar], @record.fetch_associated_records
|
51
56
|
end
|
57
|
+
|
58
|
+
def test_cache_invalidation_expire_properly_if_child_is_embed_in_multiple_parents
|
59
|
+
Item.cache_has_many :associated_records, :embed => true
|
60
|
+
ItemTwo.cache_has_many :associated_records, :embed => true
|
61
|
+
|
62
|
+
baz = AssociatedRecord.new(:name => 'baz')
|
63
|
+
|
64
|
+
record1 = Item.new(:title => 'foo')
|
65
|
+
record1.associated_records << baz
|
66
|
+
record1.save!
|
67
|
+
|
68
|
+
record2 = ItemTwo.new(:title => 'bar')
|
69
|
+
record2.associated_records << baz
|
70
|
+
record2.save!
|
71
|
+
|
72
|
+
record1.class.fetch(record1.id)
|
73
|
+
record2.class.fetch(record2.id)
|
74
|
+
|
75
|
+
expected_keys = [
|
76
|
+
record1.primary_cache_index_key,
|
77
|
+
record2.primary_cache_index_key,
|
78
|
+
]
|
79
|
+
|
80
|
+
expected_keys.each do |expected_key|
|
81
|
+
assert IdentityCache.cache.fetch(expected_key) { nil }
|
82
|
+
end
|
83
|
+
|
84
|
+
baz.save!
|
85
|
+
|
86
|
+
expected_keys.each do |expected_key|
|
87
|
+
refute IdentityCache.cache.fetch(expected_key) { nil }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_cache_invalidation_expire_properly_if_child_is_embed_in_multiple_parents_with_ids
|
92
|
+
Item.cache_has_many :associated_records, :embed => :ids
|
93
|
+
ItemTwo.cache_has_many :associated_records, :embed => :ids
|
94
|
+
|
95
|
+
baz = AssociatedRecord.new(:name => 'baz')
|
96
|
+
|
97
|
+
record1 = Item.new(:title => 'foo')
|
98
|
+
record1.save
|
99
|
+
|
100
|
+
record2 = ItemTwo.new(:title => 'bar')
|
101
|
+
record2.save
|
102
|
+
|
103
|
+
record1.class.fetch(record1.id)
|
104
|
+
record2.class.fetch(record2.id)
|
105
|
+
|
106
|
+
expected_keys = [
|
107
|
+
record1.primary_cache_index_key,
|
108
|
+
record2.primary_cache_index_key,
|
109
|
+
]
|
110
|
+
|
111
|
+
expected_keys.each do |expected_key|
|
112
|
+
assert IdentityCache.cache.fetch(expected_key) { nil }
|
113
|
+
end
|
114
|
+
|
115
|
+
baz.item = record1
|
116
|
+
baz.item_two = record2
|
117
|
+
baz.save!
|
118
|
+
|
119
|
+
expected_keys.each do |expected_key|
|
120
|
+
refute IdentityCache.cache.fetch(expected_key) { nil }
|
121
|
+
end
|
122
|
+
end
|
52
123
|
end
|
@@ -67,7 +67,7 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
|
|
67
67
|
def test_cached_associations_after_commit_hook_will_not_fail_on_undefined_parent_association
|
68
68
|
ar = AssociatedRecord.new
|
69
69
|
ar.save
|
70
|
-
assert_nothing_raised { ar.
|
70
|
+
assert_nothing_raised { ar.expire_parent_caches }
|
71
71
|
end
|
72
72
|
|
73
73
|
def test_cache_without_guessable_inverse_name_raises
|
@@ -96,7 +96,7 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
|
|
96
96
|
def test_cached_associations_after_commit_hook_will_not_fail_on_undefined_parent_association
|
97
97
|
ar = AssociatedRecord.new
|
98
98
|
ar.save
|
99
|
-
assert_nothing_raised { ar.
|
99
|
+
assert_nothing_raised { ar.expire_parent_caches }
|
100
100
|
end
|
101
101
|
|
102
102
|
def test_cache_without_guessable_inverse_name_raises
|
data/test/fetch_test.rb
CHANGED
@@ -19,7 +19,7 @@ class FetchTest < IdentityCache::TestCase
|
|
19
19
|
|
20
20
|
def test_fetch_with_garbage_input
|
21
21
|
Item.connection.expects(:exec_query)
|
22
|
-
.with(
|
22
|
+
.with(Item.where(id: 0).limit(1).to_sql, any_parameters)
|
23
23
|
.returns(ActiveRecord::Result.new([], []))
|
24
24
|
|
25
25
|
assert_equal nil, Item.fetch_by_id('garbage')
|
@@ -188,4 +188,15 @@ class FetchTest < IdentityCache::TestCase
|
|
188
188
|
fetcher.expects(:add).never
|
189
189
|
assert_raises(ActiveRecord::RecordNotFound) { Item.fetch(nil) }
|
190
190
|
end
|
191
|
+
|
192
|
+
def test_fetch_cache_hit_does_not_checkout_database_connection
|
193
|
+
@record.save!
|
194
|
+
record = Item.fetch(@record.id)
|
195
|
+
|
196
|
+
ActiveRecord::Base.clear_active_connections!
|
197
|
+
|
198
|
+
assert_equal record, Item.fetch(@record.id)
|
199
|
+
|
200
|
+
assert_equal false, ActiveRecord::Base.connection_handler.active_connections?
|
201
|
+
end
|
191
202
|
end
|
Binary file
|
@@ -27,6 +27,7 @@ module ActiveRecordObjects
|
|
27
27
|
Object.send :const_set, 'AssociatedRecord', Class.new(base) {
|
28
28
|
include IdentityCache
|
29
29
|
belongs_to :item, inverse_of: :associated_records
|
30
|
+
belongs_to :item_two, inverse_of: :associated_records
|
30
31
|
has_many :deeply_associated_records
|
31
32
|
default_scope { order('id DESC') }
|
32
33
|
}
|
@@ -57,6 +58,12 @@ module ActiveRecordObjects
|
|
57
58
|
has_one :associated, :class_name => 'AssociatedRecord'
|
58
59
|
}
|
59
60
|
|
61
|
+
Object.send :const_set, 'ItemTwo', Class.new(base) {
|
62
|
+
include IdentityCache
|
63
|
+
has_many :associated_records, inverse_of: :item_two, foreign_key: :item_two_id
|
64
|
+
self.table_name = 'items2'
|
65
|
+
}
|
66
|
+
|
60
67
|
Object.send :const_set, 'KeyedRecord', Class.new(base) {
|
61
68
|
include IdentityCache
|
62
69
|
self.primary_key = "hashed_key"
|
@@ -72,6 +79,7 @@ module ActiveRecordObjects
|
|
72
79
|
Object.send :remove_const, 'AssociatedRecord'
|
73
80
|
Object.send :remove_const, 'NotCachedRecord'
|
74
81
|
Object.send :remove_const, 'Item'
|
82
|
+
Object.send :remove_const, 'ItemTwo'
|
75
83
|
Object.send :remove_const, 'KeyedRecord'
|
76
84
|
end
|
77
85
|
end
|
@@ -1,12 +1,15 @@
|
|
1
1
|
module DatabaseConnection
|
2
2
|
def self.setup
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
db_config = ENV['DATABASE_URL'] || DEFAULT_CONFIG[ENV.fetch('DB', 'mysql2')]
|
4
|
+
begin
|
5
|
+
ActiveRecord::Base.establish_connection(db_config)
|
6
|
+
ActiveRecord::Base.connection
|
7
|
+
rescue
|
8
|
+
raise unless db_config.is_a?(Hash)
|
9
|
+
ActiveRecord::Base.establish_connection(db_config.merge('database' => nil))
|
10
|
+
ActiveRecord::Base.connection.create_database(db_config['database'])
|
11
|
+
ActiveRecord::Base.establish_connection(db_config)
|
12
|
+
end
|
10
13
|
end
|
11
14
|
|
12
15
|
def self.drop_tables
|
@@ -30,7 +33,7 @@ module DatabaseConnection
|
|
30
33
|
TABLES = {
|
31
34
|
:polymorphic_records => [[:string, :owner_type], [:integer, :owner_id], [:timestamps]],
|
32
35
|
:deeply_associated_records => [[:string, :name], [:integer, :associated_record_id], [:timestamps]],
|
33
|
-
:associated_records => [[:string, :name], [:integer, :item_id]],
|
36
|
+
:associated_records => [[:string, :name], [:integer, :item_id], [:integer, :item_two_id]],
|
34
37
|
:normalized_associated_records => [[:string, :name], [:integer, :item_id], [:timestamps]],
|
35
38
|
:not_cached_records => [[:string, :name], [:integer, :item_id], [:timestamps]],
|
36
39
|
:items => [[:integer, :item_id], [:string, :title], [:timestamps]],
|
@@ -38,10 +41,18 @@ module DatabaseConnection
|
|
38
41
|
:keyed_records => [[:string, :value], :primary_key => "hashed_key"],
|
39
42
|
}
|
40
43
|
|
41
|
-
|
42
|
-
'
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
DEFAULT_CONFIG = {
|
45
|
+
'mysql2' => {
|
46
|
+
'adapter' => 'mysql2',
|
47
|
+
'database' => 'identity_cache_test',
|
48
|
+
'host' => '127.0.0.1',
|
49
|
+
'username' => 'root'
|
50
|
+
},
|
51
|
+
'postgresql' => {
|
52
|
+
'adapter' => 'postgresql',
|
53
|
+
'database' => 'identity_cache_test',
|
54
|
+
'host' => '127.0.0.1',
|
55
|
+
'username' => 'postgres'
|
56
|
+
}
|
46
57
|
}
|
47
58
|
end
|
@@ -9,9 +9,6 @@ require_relative 'database_connection'
|
|
9
9
|
require_relative 'active_record_objects'
|
10
10
|
require 'identity_cache'
|
11
11
|
|
12
|
-
$memcached_port = 11211
|
13
|
-
$mysql_port = 3306
|
14
|
-
|
15
12
|
include SerializationFormat
|
16
13
|
include ActiveRecordObjects
|
17
14
|
|
@@ -19,7 +16,7 @@ DatabaseConnection.setup
|
|
19
16
|
DatabaseConnection.drop_tables
|
20
17
|
DatabaseConnection.create_tables
|
21
18
|
IdentityCache.logger = Logger.new(nil)
|
22
|
-
IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost
|
19
|
+
IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost:11211", :support_cas => true)
|
23
20
|
setup_models
|
24
21
|
File.open(serialized_record_file, 'w') {|file| serialize(serialized_record, file) }
|
25
22
|
puts "Serialized record to #{serialized_record_file}"
|
data/test/identity_cache_test.rb
CHANGED
@@ -14,4 +14,13 @@ class IdentityCacheTest < IdentityCache::TestCase
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
def test_should_use_cache_outside_transaction
|
18
|
+
assert_equal true, IdentityCache.should_use_cache?
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_should_use_cache_in_transaction
|
22
|
+
ActiveRecord::Base.transaction do
|
23
|
+
assert_equal false, IdentityCache.should_use_cache?
|
24
|
+
end
|
25
|
+
end
|
17
26
|
end
|
data/test/index_cache_test.rb
CHANGED
@@ -15,7 +15,7 @@ class IndexCacheTest < IdentityCache::TestCase
|
|
15
15
|
Item.cache_index :title, :id
|
16
16
|
|
17
17
|
Item.connection.expects(:exec_query)
|
18
|
-
.with(
|
18
|
+
.with(Item.select(:id).where(title: 'garbage', id: 0).to_sql, any_parameters)
|
19
19
|
.returns(ActiveRecord::Result.new([], []))
|
20
20
|
|
21
21
|
assert_equal [], Item.fetch_by_title_and_id('garbage', 'garbage')
|
@@ -25,7 +25,7 @@ class IndexCacheTest < IdentityCache::TestCase
|
|
25
25
|
Item.cache_index :title, :id, :unique => true
|
26
26
|
|
27
27
|
Item.connection.expects(:exec_query)
|
28
|
-
.with(regexp_matches(/ LIMIT 1\Z/i),
|
28
|
+
.with(regexp_matches(/ LIMIT 1\Z/i), any_parameters)
|
29
29
|
.returns(ActiveRecord::Result.new([], []))
|
30
30
|
|
31
31
|
assert_equal nil, Item.fetch_by_title_and_id('title', '2')
|
@@ -87,7 +87,7 @@ class IndexCacheTest < IdentityCache::TestCase
|
|
87
87
|
def test_non_unique_index_fetches_multiple_records
|
88
88
|
Item.cache_index :title
|
89
89
|
@record.save!
|
90
|
-
record2 = Item.create(:
|
90
|
+
record2 = Item.create(:title => 'bob') { |item| item.id = 2 }
|
91
91
|
|
92
92
|
assert_equal [@record, record2], Item.fetch_by_title('bob')
|
93
93
|
assert_equal [1, 2], backend.read(@cache_key)
|
data/test/readonly_test.rb
CHANGED
@@ -3,7 +3,6 @@ require "test_helper"
|
|
3
3
|
class ReadonlyTest < IdentityCache::TestCase
|
4
4
|
def setup
|
5
5
|
super
|
6
|
-
IdentityCache.readonly = true
|
7
6
|
@key, @value = 'foo', 'bar'
|
8
7
|
@record = Item.new
|
9
8
|
@record.id = 1
|
@@ -11,6 +10,8 @@ class ReadonlyTest < IdentityCache::TestCase
|
|
11
10
|
@bob = Item.create!(:title => 'bob')
|
12
11
|
@joe = Item.create!(:title => 'joe')
|
13
12
|
@fred = Item.create!(:title => 'fred')
|
13
|
+
IdentityCache.cache.clear
|
14
|
+
IdentityCache.readonly = true
|
14
15
|
end
|
15
16
|
|
16
17
|
def teardown
|
@@ -25,20 +26,16 @@ class ReadonlyTest < IdentityCache::TestCase
|
|
25
26
|
assert_nil backend.read(@key)
|
26
27
|
end
|
27
28
|
|
28
|
-
def
|
29
|
+
def test_delete_should_update_cache
|
29
30
|
backend.write(@key, @value)
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
assert_equal @value, backend.read(@key)
|
31
|
+
fetcher.delete(@key)
|
32
|
+
assert_equal deleted_value, backend.read(@key)
|
34
33
|
end
|
35
34
|
|
36
|
-
def
|
35
|
+
def test_clear_should_update_cache
|
37
36
|
backend.write(@key, @value)
|
38
|
-
|
39
|
-
|
40
|
-
end
|
41
|
-
assert_equal @value, backend.read(@key)
|
37
|
+
fetcher.clear
|
38
|
+
assert_equal nil, backend.read(@key)
|
42
39
|
end
|
43
40
|
|
44
41
|
def test_fetch_should_not_update_cache
|
@@ -76,6 +73,10 @@ class ReadonlyTest < IdentityCache::TestCase
|
|
76
73
|
yield
|
77
74
|
assert cas_multi.has_been_called?
|
78
75
|
end
|
76
|
+
|
77
|
+
def deleted_value
|
78
|
+
IdentityCache::DELETED
|
79
|
+
end
|
79
80
|
end
|
80
81
|
|
81
82
|
class FallbackReadonlyTest < ReadonlyTest
|
@@ -101,4 +102,8 @@ class FallbackReadonlyTest < ReadonlyTest
|
|
101
102
|
assert read_multi.has_been_called?
|
102
103
|
refute write.has_been_called?
|
103
104
|
end
|
105
|
+
|
106
|
+
def deleted_value
|
107
|
+
nil
|
108
|
+
end
|
104
109
|
end
|
data/test/save_test.rb
CHANGED
@@ -16,21 +16,21 @@ class SaveTest < IdentityCache::TestCase
|
|
16
16
|
@record = Item.new
|
17
17
|
@record.title = 'bob'
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
expect_cache_delete("#{NAMESPACE}index:Item:id/title:#{cache_hash('2/bob')}")
|
20
|
+
expect_cache_delete("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
|
21
|
+
expect_cache_delete("#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:2").once
|
22
22
|
@record.save
|
23
23
|
end
|
24
24
|
|
25
25
|
def test_update
|
26
26
|
# Regular flow, write index id, write index id/tile, delete data blob since Record has changed
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
expect_cache_delete("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/fred')}")
|
28
|
+
expect_cache_delete("#{NAMESPACE}index:Item:title:#{cache_hash('fred')}")
|
29
|
+
expect_cache_delete(@blob_key)
|
30
30
|
|
31
31
|
# Delete index id, delete index id/title
|
32
|
-
|
33
|
-
|
32
|
+
expect_cache_delete("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
|
33
|
+
expect_cache_delete("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
|
34
34
|
|
35
35
|
@record.title = 'fred'
|
36
36
|
@record.save
|
@@ -38,18 +38,18 @@ class SaveTest < IdentityCache::TestCase
|
|
38
38
|
|
39
39
|
def test_destroy
|
40
40
|
# Regular flow: delete data blob, delete index id, delete index id/tile
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
expect_cache_delete("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
|
42
|
+
expect_cache_delete("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
|
43
|
+
expect_cache_delete(@blob_key)
|
44
44
|
|
45
45
|
@record.destroy
|
46
46
|
end
|
47
47
|
|
48
48
|
def test_destroy_with_changed_attributes
|
49
49
|
# Make sure to delete the old cache index key, since the new title never ended up in an index
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
expect_cache_delete("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
|
51
|
+
expect_cache_delete("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
|
52
|
+
expect_cache_delete(@blob_key)
|
53
53
|
|
54
54
|
@record.title = 'fred'
|
55
55
|
@record.destroy
|
@@ -57,10 +57,26 @@ class SaveTest < IdentityCache::TestCase
|
|
57
57
|
|
58
58
|
def test_touch_will_expire_the_caches
|
59
59
|
# Regular flow: delete data blob, delete index id, delete index id/tile
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
expect_cache_delete("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
|
61
|
+
expect_cache_delete("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
|
62
|
+
expect_cache_delete(@blob_key)
|
63
63
|
|
64
64
|
@record.touch
|
65
65
|
end
|
66
|
+
|
67
|
+
def test_expire_cache_works_in_a_transaction
|
68
|
+
expect_cache_delete("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
|
69
|
+
expect_cache_delete("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
|
70
|
+
expect_cache_delete(@blob_key)
|
71
|
+
|
72
|
+
ActiveRecord::Base.transaction do
|
73
|
+
@record.send(:expire_cache)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def expect_cache_delete(key)
|
80
|
+
@backend.expects(:write).with(key, IdentityCache::DELETED, anything)
|
81
|
+
end
|
66
82
|
end
|
data/test/test_helper.rb
CHANGED
@@ -10,9 +10,6 @@ require 'active_support/cache/memcached_store'
|
|
10
10
|
|
11
11
|
require File.dirname(__FILE__) + '/../lib/identity_cache'
|
12
12
|
|
13
|
-
$memcached_port = 11211
|
14
|
-
$mysql_port = 3306
|
15
|
-
|
16
13
|
DatabaseConnection.setup
|
17
14
|
ActiveSupport::Cache::Store.instrument = true
|
18
15
|
|
@@ -29,15 +26,14 @@ end
|
|
29
26
|
|
30
27
|
class IdentityCache::TestCase < MiniTest::Unit::TestCase
|
31
28
|
include ActiveRecordObjects
|
32
|
-
attr_reader :backend
|
29
|
+
attr_reader :backend
|
33
30
|
|
34
31
|
def setup
|
35
32
|
DatabaseConnection.drop_tables
|
36
33
|
DatabaseConnection.create_tables
|
37
34
|
|
38
35
|
IdentityCache.logger = Logger.new(nil)
|
39
|
-
IdentityCache.cache_backend = @backend = ActiveSupport::Cache::MemcachedStore.new("localhost
|
40
|
-
@fetcher = IdentityCache.cache.cache_fetcher
|
36
|
+
IdentityCache.cache_backend = @backend = ActiveSupport::Cache::MemcachedStore.new("localhost:11211", :support_cas => true)
|
41
37
|
|
42
38
|
setup_models
|
43
39
|
end
|
@@ -47,6 +43,12 @@ class IdentityCache::TestCase < MiniTest::Unit::TestCase
|
|
47
43
|
teardown_models
|
48
44
|
end
|
49
45
|
|
46
|
+
private
|
47
|
+
|
48
|
+
def fetcher
|
49
|
+
IdentityCache.cache.cache_fetcher
|
50
|
+
end
|
51
|
+
|
50
52
|
def assert_nothing_raised
|
51
53
|
yield
|
52
54
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: identity_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Camilo Lopez
|
@@ -36,7 +36,7 @@ cert_chain:
|
|
36
36
|
fl3hbtVFTqbOlwL9vy1fudXcolIE/ZTcxQ+er07ZFZdKCXayR9PPs64heamfn0fp
|
37
37
|
TConQSX2BnZdhIEYW+cKzEC/bLc=
|
38
38
|
-----END CERTIFICATE-----
|
39
|
-
date:
|
39
|
+
date: 2015-01-06 00:00:00.000000000 Z
|
40
40
|
dependencies:
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: ar_transaction_changes
|
@@ -178,6 +178,20 @@ dependencies:
|
|
178
178
|
- - ">="
|
179
179
|
- !ruby/object:Gem::Version
|
180
180
|
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: pg
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
181
195
|
- !ruby/object:Gem::Dependency
|
182
196
|
name: stackprof
|
183
197
|
requirement: !ruby/object:Gem::Requirement
|
@@ -202,10 +216,13 @@ files:
|
|
202
216
|
- ".gitignore"
|
203
217
|
- ".ruby-version"
|
204
218
|
- ".travis.yml"
|
205
|
-
- CHANGELOG
|
219
|
+
- CHANGELOG.md
|
220
|
+
- CONTRIBUTING.md
|
206
221
|
- Gemfile
|
222
|
+
- Gemfile.rails32
|
223
|
+
- Gemfile.rails40
|
207
224
|
- Gemfile.rails41
|
208
|
-
-
|
225
|
+
- Gemfile.rails42
|
209
226
|
- LICENSE
|
210
227
|
- README.md
|
211
228
|
- Rakefile
|
metadata.gz.sig
CHANGED
Binary file
|
data/CHANGELOG
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
Unreleased
|
2
|
-
* Consistently use ActiveRecord / Arel APIs to build SQL queries
|
3
|
-
[related #148]
|
4
|
-
|
5
|
-
* Stop expiring cache on after_touch callback. [backwards incompatible]
|
6
|
-
* fetch_multi accepts an array of keys as argument
|
7
|
-
* Fix: SystemStackError when fetching more records than the max stack size
|
8
|
-
* Fix: Bug in fetch_multi in a transaction where results weren't compacted.
|
9
|
-
* Fix: Avoid unused preload on fetch_multi with :includes option for cache miss
|
10
|
-
* Change :embed option value from false to :ids for cache_has_many for clarity
|
11
|
-
* Fix: reload will invalidate the local instance cache
|
12
|
-
|
13
|
-
0.0.7
|
14
|
-
* Fix: Not implemented error for cache_has_one with embed: false
|
15
|
-
* Fix: cache key to change when adding a cache_has_many association with :embed => false
|
16
|
-
* Add support for non-integer primary keys
|
17
|
-
* Fix: Compatibility rails 4.1 for quote_value, which needs default column.
|
18
|
-
|
19
|
-
0.0.6
|
20
|
-
* Fix: bug where previously nil-cached attribute caches weren't expired on record creation
|
21
|
-
* Fix: cache key to not change when adding a non-embedded association.
|
22
|
-
* Perf: Rails 4 Only create CollectionProxy when using it
|
23
|
-
|
24
|
-
0.0.5
|
25
|
-
|
26
|
-
|
27
|
-
0.0.4
|
28
|
-
* Fix: only marshal attributes, embedded associations and normalized association IDs
|
29
|
-
* Add cache version number to cache keys
|
30
|
-
* Add test case to ensure version number is updated when the marshalled format changes
|
31
|
-
|
32
|
-
0.0.3
|
33
|
-
* Fix: memoization for multi hits actually work
|
34
|
-
* Fix: quotes SELECT projection elements on cache misses
|
35
|
-
* Add CPU performance benchmark
|
36
|
-
* Fix: table names are not hardcoded anymore
|
37
|
-
* Logger now differentiates memoized vs non memoized hits
|
38
|
-
|
39
|
-
0.0.2
|
40
|
-
* Fix: Existent embedded entries will no longer raise when ActiveModel::MissingAttributeError when accessing a newly created attribute.
|
41
|
-
* Fix: Do not marshal raw AcriveRecord associations
|