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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e49bcbf1656c4c055a0c04174a770650888b3349
4
- data.tar.gz: ea93b60ce00ff52ea21440ce811a0f1c545cb8b2
3
+ metadata.gz: 9ae6ab233ca71a68a76c92e46a6819af91b02c39
4
+ data.tar.gz: 1bf83f15e3957a5b20133cdad5cc80d675496973
5
5
  SHA512:
6
- metadata.gz: e31e64a5b480adafd760b0364d4881d22fcb4ca09eed09bde2c810d63d5c4db959a06812bd6ec8ff8ebb5f3b58e55b30fde78c2b0c4dfa3d018876849156c263
7
- data.tar.gz: 2fd22fce69151ea24d0836d0da19af7c36762c83e73ec7282e59b8298ee55c27e8196b706f9c64ea4152e331d123d0b8c6147d3625b452c6fbf905119bdebbe3
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
- - Gemfile32
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
@@ -1,7 +1,2 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in identity_cache.gemspec
4
2
  gemspec
5
-
6
- gem 'activerecord', '~> 4.0.4'
7
- gem 'activesupport', '~> 4.0.4'
@@ -1,6 +1,4 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in identity_cache.gemspec
4
2
  gemspec
5
3
 
6
4
  gem 'activerecord', '~> 3.2.16'
data/Gemfile.rails40 ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'activerecord', '~> 4.0.4'
5
+ gem 'activesupport', '~> 4.0.4'
data/Gemfile.rails41 CHANGED
@@ -1,7 +1,5 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in identity_cache.gemspec
4
2
  gemspec
5
3
 
6
4
  gem 'activerecord', '~> 4.1.0'
7
- gem 'activesupport', '~> 4.1.0'
5
+ gem 'activesupport', '~> 4.1.0'
data/Gemfile.rails42 ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'activerecord', '~> 4.2.0.rc1'
5
+ gem 'activesupport', '~> 4.2.0.rc1'
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
- ## Note
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
- Camilo Lopez (@camilo)
245
- Tom Burns (@boourns)
246
- Harry Brundage (@hornairs)
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.
@@ -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
@@ -69,12 +69,13 @@ module IdentityCache
69
69
  @logger || Rails.logger
70
70
  end
71
71
 
72
- def should_update_cache? # :nodoc:
73
- !readonly && should_use_cache?
72
+ def should_fill_cache? # :nodoc:
73
+ !readonly
74
74
  end
75
75
 
76
76
  def should_use_cache? # :nodoc:
77
- ActiveRecord::Base.connection.open_transactions == 0
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.should_update_cache?
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) if IdentityCache.should_update_cache?
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 if IdentityCache.should_update_cache?
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.should_update_cache?
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.should_update_cache?
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.should_update_cache?
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
- after_commit :expire_parent_cache
283
- after_touch :expire_parent_cache
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 expire_parent_cache
286
- expire_parent_cache_on_changes(:#{options[:inverse_name]}, '#{foreign_key}', #{parent_class}, #{options[:only_on_foreign_key_change]})
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.should_update_cache?
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) if IdentityCache.should_update_cache?
14
+ @cache_backend.delete(key)
15
15
  end
16
16
 
17
17
  def clear
18
- @cache_backend.clear if IdentityCache.should_update_cache?
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.should_update_cache?
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.should_update_cache?
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(:expire_parent_cache) if new_parent.respond_to?(:expire_parent_cache, true)
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(:expire_parent_cache) if old_parent.respond_to?(:expire_parent_cache, true)
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| @id_column.type_cast(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] }
@@ -1,4 +1,4 @@
1
1
  module IdentityCache
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.3"
3
3
  CACHE_VERSION = 5
4
4
  end
@@ -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
- Item.connection.expects(:exec_query)
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
- Item.connection.expects(:exec_query)
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
- Item.connection.expects(:exec_query)
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(@blob_key)
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(@blob_key)
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(@blob_key)
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("#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,item_id:integer,name:string")}:#{new_id}")
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
- Item.connection.expects(:exec_query)
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.expire_parent_cache }
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.expire_parent_cache }
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('SELECT `items`.* FROM `items` WHERE `items`.`id` = 0 LIMIT 1', anything)
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
- DATABASE_CONFIG['port'] ||= $mysql_port
4
- ActiveRecord::Base.establish_connection(DATABASE_CONFIG)
5
- ActiveRecord::Base.connection
6
- rescue
7
- ActiveRecord::Base.establish_connection(DATABASE_CONFIG.merge('database' => nil))
8
- ActiveRecord::Base.connection.create_database(DATABASE_CONFIG['database'])
9
- ActiveRecord::Base.establish_connection(DATABASE_CONFIG)
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
- DATABASE_CONFIG = {
42
- 'adapter' => 'mysql2',
43
- 'database' => 'identity_cache_test',
44
- 'host' => '127.0.0.1',
45
- 'username' => 'root'
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:#{$memcached_port}", :support_cas => true)
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}"
@@ -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
@@ -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(regexp_matches(/ WHERE `items`\.`title` = 'garbage' AND `items`\.`id` = 0\z/i), anything)
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), anything)
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(:id => 2, :title => 'bob')
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)
@@ -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 test_delete_should_not_update_cache
29
+ def test_delete_should_update_cache
29
30
  backend.write(@key, @value)
30
- assert_memcache_operations(0) do
31
- fetcher.delete(@key)
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 test_clear_should_not_update_cache
35
+ def test_clear_should_update_cache
37
36
  backend.write(@key, @value)
38
- assert_memcache_operations(0) do
39
- fetcher.clear
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
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:id/title:#{cache_hash('2/bob')}")
20
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
21
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:2").once
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
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/fred')}")
28
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:title:#{cache_hash('fred')}")
29
- IdentityCache.cache.expects(:delete).with(@blob_key)
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
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
33
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
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
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
42
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
43
- IdentityCache.cache.expects(:delete).with(@blob_key)
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
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
51
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
52
- IdentityCache.cache.expects(:delete).with(@blob_key)
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
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:id/title:#{cache_hash('1/bob')}")
61
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}index:Item:title:#{cache_hash('bob')}")
62
- IdentityCache.cache.expects(:delete).with(@blob_key)
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, :fetcher
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:#{$memcached_port}", :support_cas => true)
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.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: 2014-09-10 00:00:00.000000000 Z
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
- - Gemfile32
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