incrdecr_cached_counts 0.2.1 → 0.3.0

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: 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