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 +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/activerecord_cached/activerecord_extensions.rb +2 -7
- data/lib/activerecord_cached/cache.rb +38 -22
- data/lib/activerecord_cached/configuration.rb +4 -2
- data/lib/activerecord_cached/limit_checks.rb +2 -2
- data/lib/activerecord_cached/redis_mutex.rb +31 -0
- data/lib/activerecord_cached/version.rb +1 -1
- data/lib/activerecord_cached.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e2aee53321cb1679b2d0a98406614eb9c718ae07fa80a6c156ccfcb2ecb4a5d
|
4
|
+
data.tar.gz: a36a4dc08fdb12b3020c44f4ab0210f0754c3504db1a22c4712d4c2b7a53adab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
33
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
log_model_cache_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
|
-
|
14
|
-
|
13
|
+
synchronize do
|
14
|
+
hash = fetch_cache_keys_per_model
|
15
|
+
return unless model_keys = hash.delete(model)&.keys
|
15
16
|
|
16
|
-
|
17
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
42
|
+
def synchronize(&block)
|
43
|
+
cache_store_semaphore.synchronize(&block)
|
44
|
+
end
|
45
|
+
|
46
|
+
require 'monitor'
|
47
|
+
MONITOR = Monitor.new
|
37
48
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
cache_store.
|
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
|
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::
|
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
|
data/lib/activerecord_cached.rb
CHANGED
@@ -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.
|
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-
|
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:
|