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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04c55e34ac5e0676e81456a04f7edae71cf0c79c6cd5be9e135c56561f6439be
4
- data.tar.gz: 6f9d9bc0bd404cd350f1cf7ff48b8c59bbff60dbeeda754c052ca22457720b8a
3
+ metadata.gz: edca690bc194349e3a062521687f2e91fc91415f66e7f0e976293a0e7470f8a8
4
+ data.tar.gz: 78e3725f253be829e474602792fb3316889ffd8d6485bff0b1a6baae2525d0d7
5
5
  SHA512:
6
- metadata.gz: 1eec12fc16cf629cbe1b3502918b67d6d420bb812c1ed88088a809b07faf31ec981dfddb0d094507f0cd9522238134b1bc60ff011f0d99a12339144a5ba0a3d2
7
- data.tar.gz: 6a93852da524a5e478166bc24d0bee53973a3343a8b55c2574ed67259f3e5ffc7f927b31bd35e257fe0107073e69fc32a99e608614b07ab736fa103e8ce33adc
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 Recording < ApplicationRecord
33
- caches :slow_method, :really_slow_method, expires_in: 1.hour, must_revalidate: true
34
- caches :incredibly_slow_method, expires_in: 2.hours, key: -> { "custom-key/#{id}" }
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 slow_method
36
+ def amplify
37
37
  ...
38
38
  end
39
39
 
40
- def really_slow_method
40
+ def enhance_hilarity
41
41
  ...
42
42
  end
43
43
 
44
- def incredibly_slow_method
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 Example
64
- # Generate a new instance using an empty initializer (Example.new)
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 :slow_method, serializer: :new_instance
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 :slow_method, serializer: :marshal
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
 
@@ -0,0 +1,9 @@
1
+ class CacheKeeper::AutorefreshJob < CacheKeeper::BaseJob
2
+ queue_as { CacheKeeper.configuration.queues[:refresh] }
3
+
4
+ def perform
5
+ CacheKeeper.manager.cached_methods.autorefreshed.each do |cached_method|
6
+ cached_method.autorefresh_block.call
7
+ end
8
+ end
9
+ end
@@ -8,4 +8,10 @@ module CacheKeeper::CachedMethod::Refreshable
8
8
  def refresh_later(target)
9
9
  CacheKeeper::RefreshJob.perform_later self, target
10
10
  end
11
+
12
+ def autorefresh(target)
13
+ return unless stale?(target)
14
+
15
+ refresh_later target
16
+ end
11
17
  end
@@ -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].present?
43
- options[:key].is_a?(Proc) ? target.instance_exec(&options[:key]) : 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
@@ -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
@@ -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
@@ -8,7 +8,7 @@ module CacheKeeper
8
8
 
9
9
  def autorefreshed
10
10
  select do |cached_method|
11
- cached_method.options[:autorefresh].present?
11
+ cached_method.autorefresh_block.present?
12
12
  end
13
13
  end
14
14
  end
@@ -1,3 +1,3 @@
1
1
  module CacheKeeper
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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
- recording = Recording.create(number: 5)
6
- cached_method = manager.handle(Recording, :another_method, expires_in: 1.hour)
7
- manager.activate_if_handling(Recording, :another_method)
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
- result = cached_method.call(recording)
10
+ result = cached_method.call(recording)
10
11
 
11
- assert_equal 5, result
12
- assert cache_has_key? "CacheKeeper/recordings/#{recording.id}/another_method"
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, { autorefresh: true })
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.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-25 00:00:00.000000000 Z
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