cache_keeper 0.4.0 → 0.5.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 +4 -4
- data/README.md +48 -12
- data/app/jobs/cache_keeper/autorefresh_job.rb +9 -0
- data/app/models/cache_keeper/cached_method/refreshable.rb +6 -0
- data/app/models/cache_keeper/cached_method.rb +15 -4
- data/lib/cache_keeper/caching.rb +2 -2
- data/lib/cache_keeper/manager.rb +2 -2
- data/lib/cache_keeper/store.rb +1 -1
- data/lib/cache_keeper/version.rb +1 -1
- data/test/models/cached_method/refreshable_test.rb +23 -0
- data/test/models/cached_method_test.rb +76 -6
- data/test/store_test.rb +1 -1
- 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: edca690bc194349e3a062521687f2e91fc91415f66e7f0e976293a0e7470f8a8
|
4
|
+
data.tar.gz: 78e3725f253be829e474602792fb3316889ffd8d6485bff0b1a6baae2525d0d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03a22be5703c831beeb55f3a30e3f2a7534afdccf581cbb16e0f556eedc9b518c279267a4e2db06d8dfb9bcfe255f70f68313e9118b53f5522059168a75ecd70
|
7
|
+
data.tar.gz: 5cc2b635e0766a2b4dc269cf59aef03c27f19fb1ba516771885df0157835057f1479816f0110d1fbfffb3794d9510e567386e7320864c48505cff3af40058cc2
|
data/README.md
CHANGED
@@ -29,19 +29,19 @@ bundle add cache_keeper
|
|
29
29
|
CacheKeeper provides a `caches` method that will cache the result of the methods you give it:
|
30
30
|
|
31
31
|
```ruby
|
32
|
-
class
|
33
|
-
caches :
|
34
|
-
caches :
|
32
|
+
class AlienAnecdoteAmplifier < ApplicationRecord
|
33
|
+
caches :amplify, :enhance_hilarity, expires_in: 1.hour
|
34
|
+
caches :generate_anecdotal_tales, expires_in: 2.hours, must_revalidate: true
|
35
35
|
|
36
|
-
def
|
36
|
+
def amplify
|
37
37
|
...
|
38
38
|
end
|
39
39
|
|
40
|
-
def
|
40
|
+
def enhance_hilarity
|
41
41
|
...
|
42
42
|
end
|
43
43
|
|
44
|
-
def
|
44
|
+
def generate_anecdotal_tales
|
45
45
|
...
|
46
46
|
end
|
47
47
|
end
|
@@ -51,28 +51,64 @@ It's automatically available in your ActiveRecord models and in your controllers
|
|
51
51
|
|
52
52
|
By default, it will immediately run the method call if it hasn't been cached before. The next time it is called, it will return the cached value if it hasn't expired yet. If it has expired, it will enqueue a job to refresh the cache in the background and return the stale value in the meantime. You can avoid returning stale values by setting `must_revalidate: true` in the options.
|
53
53
|
|
54
|
-
CacheKeeper will compose cache keys from the name of the method and the instance's `cache_key` if it's defined or the name of the class otherwise. You can pass a `key` option to customize the cache key if you need it. It accepts [the same values](https://guides.rubyonrails.org/caching_with_rails.html#cache-keys) as `Rails.cache.fetch`, as well as procs or lambdas in case you need access to the instance.
|
55
|
-
|
56
54
|
It's important to note that it will only work with methods that don't take any arguments.
|
57
55
|
|
56
|
+
### Cache key
|
57
|
+
|
58
|
+
CacheKeeper will compose cache keys from the name of the method and the instance's `cache_key` if it's defined or the name of the class otherwise. You can pass a `key` option to customize the cache key if you need it. It accepts [the same values](https://guides.rubyonrails.org/caching_with_rails.html#cache-keys) as `Rails.cache.fetch`, as well as procs or lambdas in case you need access to the instance:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class NebulaNoodleTwister
|
62
|
+
caches :twist_noodles, :dish_of_the_day, key: ->(method_name) { [:recoding, id, method_name] }
|
63
|
+
caches :synchronize_taste_buds, key: -> { [:recoding, id, :synchronize_taste_buds] }
|
64
|
+
caches :space_soup_simulation, key: :space_soup_simulation
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
58
68
|
### Serialization
|
59
69
|
|
60
70
|
CacheKeeper needs to pass the instance on which the cached method is called along to the refresh job. As any other job argument, ActiveJob requires it to be serializable. ActiveRecord instances are serializable by default, but controllers, POROs and other classes are not. CacheKeeper provides a `serializer` option that will work in most cases:
|
61
71
|
|
62
72
|
```ruby
|
63
|
-
class
|
64
|
-
# Generate a new instance using an empty initializer (
|
73
|
+
class QuantumQuackerator
|
74
|
+
# Generate a new instance using an empty initializer (QuantumQuackerator.new)
|
65
75
|
# Useful for controllers and for POROs with no arguments
|
66
|
-
caches :
|
76
|
+
caches :quackify_particles, serializer: :new_instance
|
67
77
|
|
68
78
|
# Replicate the old instance using Marshal.dump and Marshal.load
|
69
79
|
# Useful in most other cases, but make sure the dump is not too big
|
70
|
-
caches :
|
80
|
+
caches :quackify_particles, serializer: :marshal
|
71
81
|
end
|
72
82
|
```
|
73
83
|
|
74
84
|
If those options don't work for you, you can always [write custom serializers](https://guides.rubyonrails.org/active_job_basics.html#serializers) for your classes.
|
75
85
|
|
86
|
+
### Autorefresh
|
87
|
+
|
88
|
+
CacheKeeper can automatically refresh your cached methods so that they are always warm. You need to pass a block to the `caches` method that will be called periodically. It will receive a `cached_method` object that you can use to `autorefresh` the cache for a specific instance:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class LaughInducingLuminator < ApplicationRecord
|
92
|
+
caches :generate_chuckles, expires_in: 1.day do |cached_method|
|
93
|
+
find_each do { |luminator| cached_method.autorefresh luminator }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
The last step is to register the `CacheKeeper::AutorefreshJob` in whatever system you use to run jobs periodically. For example, if you use [GoodJob](https://github.com/bensheldon/good_job) you would do something like this:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
Rails.application.configure do
|
102
|
+
config.good_job.enable_cron = true
|
103
|
+
config.good_job.cron = {
|
104
|
+
cache_keeper: {
|
105
|
+
cron: "*/15 * * * *", # every 15 minutes, every day
|
106
|
+
class: "CacheKeeper::AutorefreshJob"
|
107
|
+
}
|
108
|
+
}
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
76
112
|
|
77
113
|
## Configuration
|
78
114
|
|
@@ -2,18 +2,23 @@ class CacheKeeper::CachedMethod
|
|
2
2
|
include Refreshable
|
3
3
|
include SerializableTarget
|
4
4
|
|
5
|
-
attr_accessor :klass, :method_name, :options
|
5
|
+
attr_accessor :klass, :method_name, :options, :autorefresh_block
|
6
6
|
|
7
|
-
def initialize(klass, method_name, options = {})
|
7
|
+
def initialize(klass, method_name, options = {}, &block)
|
8
8
|
self.klass = klass
|
9
9
|
self.method_name = method_name
|
10
10
|
self.options = options.with_indifferent_access
|
11
|
+
self.autorefresh_block = block
|
11
12
|
end
|
12
13
|
|
13
14
|
def alias_for_original_method
|
14
15
|
:"__#{method_name}__hooked__"
|
15
16
|
end
|
16
17
|
|
18
|
+
def stale?(target)
|
19
|
+
cache_entry(target).blank? || cache_entry(target).expired?
|
20
|
+
end
|
21
|
+
|
17
22
|
def call(target)
|
18
23
|
cache_entry = cache_entry(target)
|
19
24
|
|
@@ -39,8 +44,14 @@ class CacheKeeper::CachedMethod
|
|
39
44
|
end
|
40
45
|
|
41
46
|
def cache_key(target)
|
42
|
-
if options[:key].
|
43
|
-
options[:key].
|
47
|
+
if options[:key].is_a?(Proc)
|
48
|
+
if options[:key].arity == 1
|
49
|
+
target.instance_exec(method_name, &options[:key])
|
50
|
+
else
|
51
|
+
target.instance_exec(&options[:key])
|
52
|
+
end
|
53
|
+
elsif options[:key].present?
|
54
|
+
options[:key]
|
44
55
|
else
|
45
56
|
target.respond_to?(:cache_key) ? ["CacheKeeper", target, method_name] : ["CacheKeeper", klass, method_name]
|
46
57
|
end
|
data/lib/cache_keeper/caching.rb
CHANGED
@@ -3,9 +3,9 @@ module CacheKeeper
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
def self.caches(*method_names, **options)
|
6
|
+
def self.caches(*method_names, **options, &block)
|
7
7
|
method_names.each do |method_name|
|
8
|
-
CacheKeeper.manager.handle self, method_name, options
|
8
|
+
CacheKeeper.manager.handle self, method_name, options, &block
|
9
9
|
|
10
10
|
# If the method is already defined, we need to hook it
|
11
11
|
method_added method_name
|
data/lib/cache_keeper/manager.rb
CHANGED
@@ -10,8 +10,8 @@ module CacheKeeper
|
|
10
10
|
cached_methods.find_by(klass, method_name).present?
|
11
11
|
end
|
12
12
|
|
13
|
-
def handle(klass, method_name, options)
|
14
|
-
CacheKeeper::CachedMethod.new(klass, method_name, options).tap do |cached_method|
|
13
|
+
def handle(klass, method_name, options, &block)
|
14
|
+
CacheKeeper::CachedMethod.new(klass, method_name, options, &block).tap do |cached_method|
|
15
15
|
if unsupported_options?(cached_method)
|
16
16
|
raise "You're trying to autorefresh an ActiveRecord model, which we don't currently support."
|
17
17
|
end
|
data/lib/cache_keeper/store.rb
CHANGED
data/lib/cache_keeper/version.rb
CHANGED
@@ -11,4 +11,27 @@ class CacheKeeper::CachedMethod::RefreshableTest < ActiveSupport::TestCase
|
|
11
11
|
cached_method.refresh_later recording
|
12
12
|
end
|
13
13
|
end
|
14
|
+
|
15
|
+
test "#autorefresh enqueues a refresh job if it's stale" do
|
16
|
+
with_clean_caching do
|
17
|
+
recording = Recording.create(number: 5)
|
18
|
+
cached_method = CacheKeeper.manager.cached_methods.first
|
19
|
+
|
20
|
+
assert_enqueued_with(job: CacheKeeper::RefreshJob) do
|
21
|
+
cached_method.autorefresh recording
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
test "#autorefresh doesn't enqueue a refresh job if it's fresh" do
|
27
|
+
with_clean_caching do
|
28
|
+
recording = Recording.create(number: 5)
|
29
|
+
cached_method = CacheKeeper.manager.cached_methods.first
|
30
|
+
cached_method.call(recording)
|
31
|
+
|
32
|
+
assert_no_enqueued_jobs do
|
33
|
+
cached_method.autorefresh recording
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
14
37
|
end
|
@@ -2,14 +2,84 @@ require "test_helper"
|
|
2
2
|
|
3
3
|
class CacheKeeper::CachedMethodTest < ActiveSupport::TestCase
|
4
4
|
test "#call caches the result of the original method" do
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
with_clean_caching do
|
6
|
+
recording = Recording.create(number: 5)
|
7
|
+
cached_method = manager.handle(Recording, :another_method, expires_in: 1.hour)
|
8
|
+
manager.activate_if_handling(Recording, :another_method)
|
8
9
|
|
9
|
-
|
10
|
+
result = cached_method.call(recording)
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
assert_equal 5, result
|
13
|
+
assert cache_has_key? "CacheKeeper/recordings/#{recording.id}/another_method"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
test "#stale? is true for cold caches" do
|
18
|
+
with_clean_caching do
|
19
|
+
recording = Recording.create(number: 5)
|
20
|
+
cached_method = manager.handle(Recording, :another_method, expires_in: 1.hour)
|
21
|
+
|
22
|
+
assert cached_method.stale?(recording)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
test "#stale? is true for expired caches" do
|
27
|
+
with_clean_caching do
|
28
|
+
recording = Recording.create(number: 5)
|
29
|
+
cached_method = manager.handle(Recording, :another_method, expires_in: 0.01.seconds)
|
30
|
+
manager.activate_if_handling(Recording, :another_method)
|
31
|
+
cached_method.call(recording)
|
32
|
+
|
33
|
+
sleep 0.01
|
34
|
+
|
35
|
+
assert cached_method.stale?(recording)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
test "#stale? is false for fresh caches" do
|
40
|
+
with_clean_caching do
|
41
|
+
recording = Recording.create(number: 5)
|
42
|
+
cached_method = manager.handle(Recording, :another_method, expires_in: 1.hour)
|
43
|
+
manager.activate_if_handling(Recording, :another_method)
|
44
|
+
cached_method.call(recording)
|
45
|
+
|
46
|
+
assert_not cached_method.stale?(recording)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
test ":key option accepts arrays" do
|
51
|
+
cached_method = manager.handle(Recording, :another_method, key: ["QuantumQuackerator", "quackify_particles"])
|
52
|
+
cache_key = cached_method.send :cache_key, Recording.new
|
53
|
+
|
54
|
+
assert_equal ["QuantumQuackerator", "quackify_particles"], cache_key
|
55
|
+
end
|
56
|
+
|
57
|
+
test ":key option accepts procs with no arguments" do
|
58
|
+
cached_method = manager.handle(Recording, :another_method, key: proc { 123 })
|
59
|
+
cache_key = cached_method.send :cache_key, Recording.new
|
60
|
+
|
61
|
+
assert_equal 123, cache_key
|
62
|
+
end
|
63
|
+
|
64
|
+
test ":key option accepts procs with an argument" do
|
65
|
+
cached_method = manager.handle(Recording, :another_method, key: proc { |method_name| [method_name, 123] })
|
66
|
+
cache_key = cached_method.send :cache_key, Recording.new
|
67
|
+
|
68
|
+
assert_equal [:another_method, 123], cache_key
|
69
|
+
end
|
70
|
+
|
71
|
+
test ":key option accepts lambdas with no arguments" do
|
72
|
+
cached_method = manager.handle(Recording, :another_method, key: -> { 123 })
|
73
|
+
cache_key = cached_method.send :cache_key, Recording.new
|
74
|
+
|
75
|
+
assert_equal 123, cache_key
|
76
|
+
end
|
77
|
+
|
78
|
+
test ":key option accepts lambdas with an argument" do
|
79
|
+
cached_method = manager.handle(Recording, :another_method, key: ->(method_name) { [method_name, 123] })
|
80
|
+
cache_key = cached_method.send :cache_key, Recording.new
|
81
|
+
|
82
|
+
assert_equal [:another_method, 123], cache_key
|
13
83
|
end
|
14
84
|
|
15
85
|
private
|
data/test/store_test.rb
CHANGED
@@ -11,7 +11,7 @@ class CacheKeeper::StoreTest < ActiveSupport::TestCase
|
|
11
11
|
|
12
12
|
test "#autorefreshed returns only the ones with the correct option" do
|
13
13
|
cached_method = CacheKeeper::CachedMethod.new(String, :slow_method, {})
|
14
|
-
autorefreshed_cached_method = CacheKeeper::CachedMethod.new(String, :really_slow_method, {
|
14
|
+
autorefreshed_cached_method = CacheKeeper::CachedMethod.new(String, :really_slow_method, {}, &proc {})
|
15
15
|
store = CacheKeeper::Store.new([cached_method, autorefreshed_cached_method])
|
16
16
|
|
17
17
|
assert_equal [autorefreshed_cached_method], store.autorefreshed
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cache_keeper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Zamuner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-10-
|
11
|
+
date: 2023-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- Gemfile
|
37
37
|
- MIT-LICENSE
|
38
38
|
- README.md
|
39
|
+
- app/jobs/cache_keeper/autorefresh_job.rb
|
39
40
|
- app/jobs/cache_keeper/base_job.rb
|
40
41
|
- app/jobs/cache_keeper/refresh_job.rb
|
41
42
|
- app/models/cache_keeper/cached_method.rb
|