activerecord_cached 1.0.0 → 1.1.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: 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: