activerecord_cached 1.0.0 → 1.1.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: 051daa89ebda43ea098a7f152e567730f1a71f4794919ac4ac3ca8ec98936526
4
- data.tar.gz: 829ea885b15719e2c77357f4caa1c6d39d4c221719fcca39a295eb41af1e0256
3
+ metadata.gz: 0e2aee53321cb1679b2d0a98406614eb9c718ae07fa80a6c156ccfcb2ecb4a5d
4
+ data.tar.gz: a36a4dc08fdb12b3020c44f4ab0210f0754c3504db1a22c4712d4c2b7a53adab
5
5
  SHA512:
6
- metadata.gz: d98b20830828b808644df5b31a6a917a40af2aba50808e003c4b2b92d08f1b27297ba140ae61a4ea82c7c8c27ac0927545576734cf2b5f05074556d4daedcadf
7
- data.tar.gz: ba34d2b20e9f07d13eb40b4bee773ed048eea9546d78e298b1ec0aa3c67c8012c170dad502b1d226d1ed3f50de54e12623f8abd6fa79d404d3e560fe2907990e
6
+ metadata.gz: ef43829aac59b7f8b9bc5a0aea7deaff9002ee532a94f91f9d5339e1160660242a3199a437c115233ba3cd6672c17f17c172f4d7cc09423000d991f118aa64c6
7
+ data.tar.gz: e428cf58d5b5f7af8b5034ec9e6b46ac6f5141a7bf8489d20604e3b47fe8287b1cd14b1109825cfa46e80179800ab459faf506f1d410916b308b0a8c8fc32529
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2024-01-25
4
+
5
+ ## Added
6
+
7
+ - cache expiry and race_condition_ttl
8
+
9
+ ## Fixed
10
+
11
+ - some race conditions that would affect cache clearing by CRUD operations
12
+ - size mentioned in warning message when using a custom MemoryStore
13
+
3
14
  ## [1.0.0] - 2024-01-23
4
15
 
5
16
  - Initial release
@@ -29,13 +29,8 @@ module ActiveRecordCached
29
29
  ActiveRecord::Base.singleton_class.prepend BaseExtension
30
30
  ActiveRecord::Relation.prepend RelationExtension
31
31
 
32
- # bust cache on individual record changes - this module is included
33
- # automatically into models that use cached methods.
34
- module CRUDCallbacks
35
- def self.included(base)
36
- base.after_commit { self.class.clear_cached_values }
37
- end
38
- end
32
+ # bust cache on individual record changes
33
+ ActiveRecord::Base.after_commit { self.class.clear_cached_values }
39
34
 
40
35
  # bust cache on mass operations
41
36
  module MassOperationWrapper
@@ -1,50 +1,66 @@
1
1
  module ActiveRecordCached
2
2
  def fetch(relation, method, args)
3
3
  key = ['ActiveRecordCached', relation.to_sql, method, args.sort].join(':')
4
- model = relation.klass
5
- prepare(model)
6
- cache_store.fetch(key) do
7
- log_model_cache_key(model, key)
4
+ cache_store.fetch(key, expires_in: 1.day + rand(300).seconds, race_condition_ttl: 10.seconds) do
5
+ # The gem keeps track of all cache keys used for each model for easy clearing.
6
+ # Using #delete_matched would be too slow on large redis instances.
7
+ log_model_cache_key(relation.klass, key)
8
8
  query_db(relation, method, args)
9
9
  end
10
10
  end
11
11
 
12
12
  def clear_for_model(model)
13
- keys = cache_keys_per_model
14
- return unless model_keys = keys.delete(model)&.keys
13
+ synchronize do
14
+ hash = fetch_cache_keys_per_model
15
+ return unless model_keys = hash.delete(model)&.keys
15
16
 
16
- cache_store.delete_multi(model_keys)
17
- cache_store.write(CACHE_KEYS_KEY, keys)
17
+ # delete from the key list first so that if a fetch comes in right after,
18
+ # it can write the key to the list again
19
+ write_cache_keys_per_model(hash)
20
+ cache_store.delete_multi(model_keys)
21
+ end
18
22
  end
19
23
 
20
24
  def clear_all
21
- all_keys = cache_keys_per_model.values.flat_map(&:keys)
22
- cache_store.delete_multi(all_keys)
23
- cache_store.delete(CACHE_KEYS_KEY)
25
+ synchronize do
26
+ all_keys = fetch_cache_keys_per_model.values.flat_map(&:keys)
27
+ write_cache_keys_per_model({})
28
+ cache_store.delete_multi(all_keys)
29
+ end
24
30
  end
25
31
 
26
32
  private
27
33
 
28
- def prepare(model)
29
- return if CRUDCallbacks.in?(model.included_modules)
30
-
31
- PREPARE_MUTEX.synchronize do
32
- CRUDCallbacks.in?(model.included_modules) || model.include(CRUDCallbacks)
34
+ def log_model_cache_key(model, key)
35
+ synchronize do
36
+ hash = fetch_cache_keys_per_model
37
+ (hash[model] ||= {})[key] = true
38
+ write_cache_keys_per_model(hash)
33
39
  end
34
40
  end
35
41
 
36
- PREPARE_MUTEX = Mutex.new
42
+ def synchronize(&block)
43
+ cache_store_semaphore.synchronize(&block)
44
+ end
45
+
46
+ require 'monitor'
47
+ MONITOR = Monitor.new
37
48
 
38
- def log_model_cache_key(model, key)
39
- keys = cache_keys_per_model
40
- (keys[model] ||= {})[key] = true
41
- cache_store.write(CACHE_KEYS_KEY, keys)
49
+ def cache_store_semaphore
50
+ case cache_store
51
+ when ActiveSupport::Cache::MemoryStore then MONITOR
52
+ when ActiveSupport::Cache::RedisCacheStore then RedisMutex.new(cache_store.redis)
53
+ end
42
54
  end
43
55
 
44
- def cache_keys_per_model
56
+ def fetch_cache_keys_per_model
45
57
  cache_store.read(CACHE_KEYS_KEY) || {}
46
58
  end
47
59
 
60
+ def write_cache_keys_per_model(val)
61
+ cache_store.write(CACHE_KEYS_KEY, val)
62
+ end
63
+
48
64
  CACHE_KEYS_KEY = 'ActiveRecordCached:cache_keys_per_model'
49
65
 
50
66
  def query_db(rel, method, args)
@@ -10,7 +10,9 @@ module ActiveRecordCached
10
10
  end
11
11
 
12
12
  def cache_store=(val)
13
- val.is_a?(ActiveSupport::Cache::Store) || raise(ArgumentError, 'pass an ActiveSupport::Cache::Store')
13
+ val.is_a?(ActiveSupport::Cache::MemoryStore) ||
14
+ val.is_a?(ActiveSupport::Cache::RedisCacheStore) ||
15
+ raise(ArgumentError, 'pass a MemoryStore or RedisCacheStore')
14
16
  @cache_store = store_with_limit_warning(val)
15
17
  end
16
18
 
@@ -45,7 +47,7 @@ module ActiveRecordCached
45
47
 
46
48
  store.singleton_class.prepend(Module.new do
47
49
  def prune(...)
48
- ActiveRecordCached.send(:warn_max_total_bytes_exceeded)
50
+ ActiveRecordCached.send(:warn_max_total_bytes_exceeded, @max_size)
49
51
  super
50
52
  end
51
53
  end)
@@ -10,8 +10,8 @@ module ActiveRecordCached
10
10
  end
11
11
  end
12
12
 
13
- def warn_max_total_bytes_exceeded
14
- warn_limit_reached("Store size >= #{max_total_bytes} max_total_bytes")
13
+ def warn_max_total_bytes_exceeded(store_size = nil)
14
+ warn_limit_reached("Store size >= #{store_size || max_total_bytes} max_total_bytes")
15
15
  end
16
16
 
17
17
  def warn_limit_reached(info)
@@ -0,0 +1,31 @@
1
+ module ActiveRecordCached
2
+ class RedisMutex
3
+ KEY = 'ActiveRecordCached:RedisMutex'
4
+ MAX_HOLD_TIME = 2
5
+ LOCK_ACQUIRER = "return redis.call('setnx', KEYS[1], 1) == 1 and redis.call('expire', KEYS[1], KEYS[2]) and 1 or 0"
6
+
7
+ def initialize(redis)
8
+ @redis = redis.then(&:itself)
9
+ end
10
+
11
+ def synchronize(&block)
12
+ acquire_lock
13
+ begin
14
+ block.call
15
+ ensure
16
+ release_lock
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def acquire_lock
23
+ sleep_t = 0.005
24
+ while @redis.eval(LOCK_ACQUIRER, [KEY, MAX_HOLD_TIME]) != 1 do sleep(sleep_t *= 1.6) end
25
+ end
26
+
27
+ def release_lock
28
+ @redis.del(KEY)
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCached
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -8,6 +8,7 @@ require_relative "activerecord_cached/cache"
8
8
  require_relative "activerecord_cached/configuration"
9
9
  require_relative "activerecord_cached/limit_checks"
10
10
  require_relative "activerecord_cached/railtie" if defined?(::Rails::Railtie)
11
+ require_relative "activerecord_cached/redis_mutex"
11
12
  require_relative "activerecord_cached/version"
12
13
 
13
14
  module ActiveRecordCached
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord_cached
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janosch Müller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-23 00:00:00.000000000 Z
11
+ date: 2024-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -56,6 +56,7 @@ files:
56
56
  - lib/activerecord_cached/configuration.rb
57
57
  - lib/activerecord_cached/limit_checks.rb
58
58
  - lib/activerecord_cached/railtie.rb
59
+ - lib/activerecord_cached/redis_mutex.rb
59
60
  - lib/activerecord_cached/version.rb
60
61
  homepage: https://github.com/jaynetics/activerecord_cached
61
62
  licenses: