incrdecr_cached_counts 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f47ca230c62c31bfaa2f488764ebb48b9e2aeefe611ba9fae8cd15d458a53e8
4
- data.tar.gz: ed2839fb88a855bd60c92a3506fe367206b74db71479732009e6769af346c9a6
3
+ metadata.gz: 42bbe0c4b1d6ddc82d92bc3d1475dcf3e7553ef9f8299d0e3d75aa5d7c8519c6
4
+ data.tar.gz: 652f35c8b0c667c3ee9517494c2a23b04ea3f90a7591d74922538c84906c754f
5
5
  SHA512:
6
- metadata.gz: 8bf76a8198b3dc430c8ff5658da2d4889aa7344dad224241af7fd82c318100932a2d81582a946b942ab908ffc3e0e60a3dbe916999c4130fecc02e3f26bbd1ca
7
- data.tar.gz: 31c5dc4eef93988cf1463827daf60b78bd19a0bf8e3ad536ee6cbe8f4f940b19ebe35c32b4027bf081e565f9c97e921336d9257f0d9a17a0d5be98786c648fdd
6
+ metadata.gz: 6610220977371d6bde5f48e6aa6dfbe879e4213d04fd0ae7bc05a00e4766751ee427b5e2f3e42ecb88fb8e665885a786f38a125a81fc042b258078e3a3debfa1
7
+ data.tar.gz: 6d8c16611f42f680f4350b196935cf0caffef9253e6b016e1731b142f5d719c3ce43f2293869efb6393cb52f61e05e4819dfdac6357fccf4f228ff05111b3306
@@ -39,11 +39,13 @@ module CachedCounts
39
39
  # @option options [Integer, #to_s] :version
40
40
  # Cache version - bump if you change the definition of a count.
41
41
  #
42
- # @option options [Lambda] :default_value_lambda
43
- # Uses this value when the cache is empty, while it is being populated
44
- #
45
- # @option options [Lambda] :value_updater_lambda
46
- # Override logic for updating a cache value - e.g. to perform background updates
42
+ # @option options [Proc] :race_condition_fallback
43
+ # Fallback to the result of this proc if the cache is empty, while
44
+ # loading the actual value from the db. Works similarly to
45
+ # +race_condition_ttl+ but for empty caches rather than expired values.
46
+ # Meant to prevent a thundering-herd scenario, if for example a
47
+ # memcached instance goes away. Can be nil; defaults to using a value
48
+ # grabbed from the cache or DB at startup.
47
49
  #
48
50
  def caches_count_where(attribute_name, options = {})
49
51
  # Delay actual run to work around circular dependencies
@@ -134,6 +136,14 @@ module CachedCounts
134
136
  version = options.fetch :version, 1
135
137
  key = scope_count_key(attribute_name, version)
136
138
 
139
+ unless options.has_key?(:race_condition_fallback)
140
+ options[:race_condition_fallback] = default_race_condition_fallback_proc(
141
+ key,
142
+ relation,
143
+ options
144
+ )
145
+ end
146
+
137
147
  [attribute_name, *Array(options[:alias])].each do |attr_name|
138
148
  add_count_attribute_methods(
139
149
  attr_name,
@@ -146,6 +156,23 @@ module CachedCounts
146
156
  end
147
157
  end
148
158
 
159
+ def default_race_condition_fallback_proc(key, relation, options)
160
+ fallback = Rails.cache.read(key)
161
+ fallback = fallback.value if fallback.is_a?(ActiveSupport::Cache::Entry)
162
+
163
+ if fallback.nil?
164
+ begin
165
+ fallback = relation.count
166
+ rescue ActiveRecord::StatementInvalid => e
167
+ fallback = 0
168
+ end
169
+
170
+ Rails.cache.write key, fallback, expires_in: options.fetch(:expires_in, 1.week), raw: true
171
+ end
172
+
173
+ -> { fallback }
174
+ end
175
+
149
176
  def define_association_count_attribute(attribute_name, association, options)
150
177
  options = options.dup
151
178
 
@@ -210,12 +237,7 @@ module CachedCounts
210
237
 
211
238
  def add_count_attribute_methods(attribute_name, key_getter, relation_getter, define_with, counted_class, options)
212
239
  expires_in = options.fetch :expires_in, 1.week
213
- default_value_lambda = options.fetch :default_value_lambda, -> {0}
214
- value_updater_lambda = options.fetch :value_updater_lambda, default_value_updater_lambda
215
- # TODO: need a good strategy to figure this value out
216
- # As a fallback value is used, we immediately fire the calculation for the real value
217
- # Is it reasonable to wait as long as it takes to calcualte it? Is there an alternative?
218
- fallback_expiry_seconds = expires_in
240
+ race_condition_fallback = options.fetch :race_condition_fallback, nil
219
241
 
220
242
  key_method = "#{attribute_name}_count_key"
221
243
 
@@ -225,23 +247,34 @@ module CachedCounts
225
247
  val = Rails.cache.fetch(
226
248
  send(key_method),
227
249
  expires_in: expires_in,
228
- race_condition_ttl: fallback_expiry_seconds,
250
+ race_condition_ttl: 30.seconds,
229
251
  raw: true # Necessary for incrementing to work correctly
230
252
  ) do
231
- # Write down the default value first so subsequent reads don't repeat calculations
232
- # TODO: multiple requests can potentially get to this point and start multiple update operations? Is that a problem?
233
- default_value = instance_exec &default_value_lambda
234
- Rails.cache.write(
235
- send(key_method),
236
- default_value,
237
- expires_in: fallback_expiry_seconds,
238
- raw: true
239
- )
253
+ if race_condition_fallback
254
+ # Ensure that other reads find something in the cache, but
255
+ # continue calculating here because the default is likely inaccurate.
256
+ fallback_value = instance_exec &race_condition_fallback
257
+ CachedCounts.logger.warn "Setting #{fallback_value} as race_condition_fallback for #{send(key_method)}"
258
+ Rails.cache.write(
259
+ send(key_method),
260
+ fallback_value.to_i,
261
+ expires_in: 30.seconds,
262
+ raw: true
263
+ )
264
+ end
265
+
240
266
  relation = instance_exec(&relation_getter)
241
267
  relation = relation.reorder('')
242
268
  relation.select_values = ['count(*)']
243
- value_updater_lambda.call(counted_class, relation.to_sql, send(key_method), expires_in) || default_value
269
+
270
+ conn = CachedCounts.connection_for(counted_class)
271
+ if Rails.version < '4.2'.freeze
272
+ conn.select_value(relation.to_sql, nil, relation.values[:bind] || []).to_i
273
+ else
274
+ conn.select_value(relation.to_sql).to_i
275
+ end
244
276
  end
277
+
245
278
  if val.is_a?(ActiveSupport::Cache::Entry)
246
279
  val.value.to_i
247
280
  else
@@ -263,20 +296,6 @@ module CachedCounts
263
296
  end
264
297
  end
265
298
 
266
- def default_value_updater_lambda()
267
- lambda { |counted_class, relation_sql, cache_key, expires_in|
268
- conn = CachedCounts.connection_for(counted_class)
269
- value = conn.select_value(relation_sql).to_i
270
- Rails.cache.write(
271
- cache_key,
272
- value,
273
- expires_in: expires_in,
274
- raw: true
275
- )
276
- return value
277
- }
278
- end
279
-
280
299
  def add_counting_hooks(attribute_name, key_getter, counted_class, options)
281
300
  increment_hook = "increment_#{attribute_name}_count".to_sym
282
301
  counted_class.send :define_method, increment_hook do
@@ -1,3 +1,3 @@
1
1
  module CachedCounts
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: incrdecr_cached_counts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Judd
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-18 00:00:00.000000000 Z
11
+ date: 2019-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -177,4 +177,13 @@ rubygems_version: 3.0.3
177
177
  signing_key:
178
178
  specification_version: 4
179
179
  summary: A replacement for Rails' counter caches using memcached (via Dalli)
180
- test_files: []
180
+ test_files:
181
+ - spec/caches_count_of_spec.rb
182
+ - spec/caches_count_where_spec.rb
183
+ - spec/database.yml
184
+ - spec/fixtures.rb
185
+ - spec/fixtures/department.rb
186
+ - spec/fixtures/following.rb
187
+ - spec/fixtures/university.rb
188
+ - spec/fixtures/user.rb
189
+ - spec/spec_helper.rb