identity_cache 1.2.0 → 1.3.1

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
  SHA256:
3
- metadata.gz: 561965856845fc5d5094581d735584924eda58557edfb3831c39bce2498635aa
4
- data.tar.gz: 33a85428a3b2a298076d81a79b662b742c00a25971bf453f8f57eeab7cba2681
3
+ metadata.gz: 3abed29487ae94532060d1a35e2bc86f8b4187cbc9a3074c8c304ab4f8426b4b
4
+ data.tar.gz: ddcaf494c9bc48055ee4e1ddc83a5caf61e047e4d053eb2b4e87a086a73f97ae
5
5
  SHA512:
6
- metadata.gz: 6a8c691d8883acfd69f11724dfff13a3ef4858a2abd2ca658659981f1d08b36dea244100c4030b8ef5d39ac8509e639fea6cd02127e09d157be636fcc2bee77a
7
- data.tar.gz: e357c3a4f02a288a5ddb3290cbd11176937821bb3c9052fb318d9139ac253650f7018a8249b2ebc9f115015a6434baf7cb1b7e76bfb0dbf1e98ee745e3e95ed7
6
+ metadata.gz: e0c8e9d0996d6f7f3f3e760fbf1ec2f7b8fa819bbeeaa3dc8b1286125265da45c6b174a77d3b13f763e6db104d613a55562f0a0ff13eb6d0c800e455ae30020c
7
+ data.tar.gz: 03a5cffffa5a0971f0d12a4d449c518a471d579410c2f7b8dfe32ad82234fb08115fa44cc99e726ed28352ead0287c5e966f47fb0b607758ddb29b1baa4667a4
@@ -16,14 +16,14 @@ jobs:
16
16
  matrix:
17
17
  entry:
18
18
  - name: 'Minimum supported'
19
- ruby: '2.5'
19
+ ruby: '2.7'
20
20
  gemfile: "Gemfile.min-supported"
21
21
  - name: 'Latest released & run rubocop'
22
- ruby: '3.0'
22
+ ruby: '3.2'
23
23
  gemfile: "Gemfile.latest-release"
24
24
  rubocop: true
25
25
  - name: 'Rails edge'
26
- ruby: '3.0'
26
+ ruby: '3.2'
27
27
  gemfile: "Gemfile.rails-edge"
28
28
  edge: true
29
29
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Identity Cache Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 1.3.1
6
+
7
+ ### Fixes
8
+ - Remove N+1 queries from embedded associations when using `fetch` while `should_use_cache` is false. (#531)
9
+
10
+ ## 1.3.0
11
+
12
+ ### Features
13
+ - Return meaningful value from `expire_cache` indicating whenever it succeeded or failed in the process. (#523)
14
+
15
+ ### Fixes
16
+ - Expire parents cache when when calling `expire_cache`. (#523)
17
+ - Avoid creating too many shapes on Ruby 3.2+. (#526)
18
+
3
19
  ## 1.2.0
4
20
 
5
21
  ### Fixes
data/README.md CHANGED
@@ -254,11 +254,26 @@ Cache keys include a version number by default, specified in `IdentityCache::CAC
254
254
 
255
255
  ## Caveats
256
256
 
257
- A word of warning. If an `after_commit` fails before the cache expiry `after_commit` the cache will not be expired and you will be left with stale data.
258
-
259
- Since everything is being marshalled and unmarshalled from Memcached changing Ruby or Rails versions could mean your objects cannot be unmarshalled from Memcached. There are a number of ways to get around this such as namespacing keys when you upgrade or rescuing marshal load errors and treating it as a cache miss. Just something to be aware of if you are using IdentityCache and upgrade Ruby or Rails.
260
-
261
- 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, exceptions happen, and network blips occur, which means there is a chance that some database transaction might commit but the corresponding memcached cache invalidation 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.
257
+ IdentityCache is never going to be 100% consistent, since cache invalidations can be lost. As such, it was intentionally designed to be _opt-in_, so it is only used where cache inconsistency is tolerated. 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 means that you need to think carefully about when you use `fetch` and when you use `find`.
258
+
259
+ Expected sources of lost cache invalidations include:
260
+ * Database write performed that doesn't trigger an after_commit callback
261
+ * Process/system getting killed or crashing between the database commit and cache invalidation
262
+ * Network unavailability, including transient failures, preventing the delivery of the cache invalidation
263
+ * Memcached unavailability or failure preventing the processing of the cache invalidation request
264
+ * Memcached flush / restart could remove a cache invalidation that would normally interrupt a cache fill that started when the cache key was absent. E.g.
265
+ 1. cache key absent (not just invalidated)
266
+ 2. process 1 reads cache key
267
+ 3. process 1 starts reading from the database
268
+ 4. process 2 writes to the database
269
+ 5. process 2 writes a cache invalidation marker to cache key
270
+ 6. memcached flush
271
+ 7. process 1 uses an `ADD` operation, which succeeds in filling the cache with the now stale data
272
+ * Rollout of cache namespace changes (e.g. from upgrading IdentityCache, adding columns, cached associations or from application changes to IdentityCache.cache_namespace) can result in cache fills to the new namespace that aren't invalidated by cache invalidations from a process still using the old namespace
273
+
274
+ Cache expiration is meant to be used to help the system recover, but it only works if the application avoids using the cache data as a transaction to write data. IdentityCache avoids loading cached data from its methods during an open transaction, but can't prevent cache data that was loaded before the transaction was opened from being used in a transaction. IdentityCache won't help with scaling write traffic, it was intended for scaling database queries from read-only requests.
275
+
276
+ IdentityCache also caches the absence of database values (e.g. to avoid performance problems when it is destroyed), so lost cache invalidations can also result in that value continuing to remain absent. As such, avoid sending the id of an uncommitted database record to another process (e.g. queuing it to a background job), since that could result in an attempt to read the record by its id before it has been created. A cache invalidation will still be attempted when the record is created, but that could be lost.
262
277
 
263
278
  ## Notes
264
279
 
data/dev.yml CHANGED
@@ -5,7 +5,7 @@ up:
5
5
  - mysql-client@5.7:
6
6
  or: [mysql@5.7]
7
7
  conflicts: [mysql-connector-c, mysql, mysql-client]
8
- - ruby: 2.7.2
8
+ - ruby: 3.2.0
9
9
  - isogun
10
10
  - bundler
11
11
 
@@ -44,7 +44,7 @@ module IdentityCache
44
44
  # @param db_key [Array] Reference to what to load from the database.
45
45
  # @return [Hash] A hash mapping each database key to its corresponding value
46
46
  def load_multi(cache_fetcher, db_keys)
47
- load_batch(cache_fetcher => db_keys).fetch(cache_fetcher)
47
+ load_batch({ cache_fetcher => db_keys }).fetch(cache_fetcher)
48
48
  end
49
49
 
50
50
  # Load multiple keys for multiple cache fetchers
@@ -35,16 +35,20 @@ module IdentityCache
35
35
  end
36
36
 
37
37
  def expire(record)
38
+ all_deleted = true
39
+
38
40
  unless record.send(:was_new_record?)
39
41
  old_key = old_cache_key(record)
40
- IdentityCache.cache.delete(old_key)
42
+ all_deleted = IdentityCache.cache.delete(old_key)
41
43
  end
42
44
  unless record.destroyed?
43
45
  new_key = new_cache_key(record)
44
46
  if new_key != old_key
45
- IdentityCache.cache.delete(new_key)
47
+ all_deleted = IdentityCache.cache.delete(new_key) && all_deleted
46
48
  end
47
49
  end
50
+
51
+ all_deleted
48
52
  end
49
53
 
50
54
  def cache_key(index_key)
@@ -45,8 +45,9 @@ module IdentityCache
45
45
  def expire_parent_caches
46
46
  parents_to_expire = Set.new
47
47
  add_parents_to_cache_expiry_set(parents_to_expire)
48
- parents_to_expire.each do |parent|
49
- parent.expire_primary_index if parent.class.primary_cache_index_enabled
48
+ parents_to_expire.select! { |parent| parent.class.primary_cache_index_enabled }
49
+ parents_to_expire.reduce(true) do |all_expired, parent|
50
+ parent.expire_primary_index && all_expired
50
51
  end
51
52
  end
52
53
 
@@ -70,6 +70,8 @@ module IdentityCache
70
70
  readonly: IdentityCache.fetch_read_only_records && should_use_cache?)
71
71
  return if records.empty?
72
72
 
73
+ return unless should_use_cache?
74
+
73
75
  records.each(&:readonly!) if readonly
74
76
  each_id_embedded_association do |cached_association|
75
77
  preload_id_embedded_association(records, cached_association)
@@ -166,15 +168,17 @@ module IdentityCache
166
168
  def _run_commit_callbacks
167
169
  if destroyed? || transaction_changed_attributes.present?
168
170
  expire_cache
169
- expire_parent_caches
170
171
  end
171
172
  super
172
173
  end
173
174
 
174
- # Invalidate the cache data associated with the record.
175
+ # Invalidate the cache data associated with the record. Returns `true` on success,
176
+ # `false` otherwise.
175
177
  def expire_cache
176
- expire_attribute_indexes
177
- true
178
+ expired_parent_caches = expire_parent_caches
179
+ expired_attribute_indexes = expire_attribute_indexes
180
+
181
+ expired_parent_caches && expired_attribute_indexes
178
182
  end
179
183
 
180
184
  # @api private
@@ -185,9 +189,11 @@ module IdentityCache
185
189
 
186
190
  private
187
191
 
192
+ # Even if we have problems with some attributes, carry over the results and expire
193
+ # all possible attributes without array allocation.
188
194
  def expire_attribute_indexes # :nodoc:
189
- cache_indexes.each do |cached_attribute|
190
- cached_attribute.expire(self)
195
+ cache_indexes.reduce(true) do |all_expired, cached_attribute|
196
+ cached_attribute.expire(self) && all_expired
191
197
  end
192
198
  end
193
199
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IdentityCache
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.1"
5
5
  CACHE_VERSION = 8
6
6
  end
@@ -7,8 +7,9 @@ module IdentityCache
7
7
  include WithoutPrimaryIndex
8
8
 
9
9
  def expire_cache
10
- expire_primary_index
11
- super
10
+ expired_primary_index = expire_primary_index
11
+
12
+ super && expired_primary_index
12
13
  end
13
14
 
14
15
  # @api private
@@ -158,6 +159,15 @@ module IdentityCache
158
159
  def expire_primary_key_cache_index(id)
159
160
  cached_primary_index.expire(id)
160
161
  end
162
+
163
+ private
164
+
165
+ def inherited(subclass)
166
+ super
167
+ subclass.class_eval do
168
+ @cached_primary_index = nil
169
+ end
170
+ end
161
171
  end
162
172
  end
163
173
  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: 1.2.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Camilo Lopez
@@ -14,7 +14,7 @@ authors:
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2022-08-15 00:00:00.000000000 Z
17
+ date: 2023-03-23 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: activerecord
@@ -190,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
190
  - !ruby/object:Gem::Version
191
191
  version: '0'
192
192
  requirements: []
193
- rubygems_version: 3.3.3
193
+ rubygems_version: 3.4.9
194
194
  signing_key:
195
195
  specification_version: 4
196
196
  summary: IdentityCache lets you specify how you want to cache your model objects,