From 5caced3650bf51bd1035347b9823367dd9095e02 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Thu, 4 Apr 2019 17:27:29 +0000 Subject: Allow reactive caching to be used in services Add support for defining a reactive_cache_worker_finder function that will be used by the reactive_caching_worker to generate/initialize the calling object. This allows reactive caching to work with Services where the object cannot be obtained from DB but a new object can be initialized. --- app/models/concerns/reactive_caching.rb | 39 +++++++++++++++++++ app/workers/reactive_caching_worker.rb | 7 ++-- .../unreleased/58375-reactive-caching-changes.yml | 5 +++ spec/models/concerns/reactive_caching_spec.rb | 44 ++++++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/58375-reactive-caching-changes.yml diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 1ab3b3ddc46..1e09cd89550 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -29,6 +29,40 @@ # However, it will enqueue a background worker to call `#calculate_reactive_cache` # and set an initial cache lifetime of ten minutes. # +# The background worker needs to find or generate the object on which +# `with_reactive_cache` was called. +# The default behaviour can be overridden by defining a custom +# `reactive_cache_worker_finder`. +# Otherwise the background worker will use the class name and primary key to get +# the object using the ActiveRecord find_by method. +# +# class Bar +# include ReactiveCaching +# +# self.reactive_cache_key = ->() { ["bar", "thing"] } +# self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) } +# +# def self.from_cache(var1, var2) +# # This method will be called by the background worker with "bar1" and +# # "bar2" as arguments. +# new(var1, var2) +# end +# +# def initialize(var1, var2) +# # ... +# end +# +# def calculate_reactive_cache +# # Expensive operation here. The return value of this method is cached +# end +# +# def result +# with_reactive_cache("bar1", "bar2") do |data| +# # ... +# end +# end +# end +# # Each time the background job completes, it stores the return value of # `#calculate_reactive_cache`. It is also re-enqueued to run again after # `reactive_cache_refresh_interval`, so keeping the stored value up to date. @@ -52,6 +86,7 @@ module ReactiveCaching class_attribute :reactive_cache_key class_attribute :reactive_cache_lifetime class_attribute :reactive_cache_refresh_interval + class_attribute :reactive_cache_worker_finder # defaults self.reactive_cache_lease_timeout = 2.minutes @@ -59,6 +94,10 @@ module ReactiveCaching self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_lifetime = 10.minutes + self.reactive_cache_worker_finder = ->(id, *_args) do + find_by(primary_key => id) + end + def calculate_reactive_cache(*args) raise NotImplementedError end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb index 9ec8bcca4f3..b30864db802 100644 --- a/app/workers/reactive_caching_worker.rb +++ b/app/workers/reactive_caching_worker.rb @@ -3,7 +3,6 @@ class ReactiveCachingWorker include ApplicationWorker - # rubocop: disable CodeReuse/ActiveRecord def perform(class_name, id, *args) klass = begin class_name.constantize @@ -12,7 +11,9 @@ class ReactiveCachingWorker end return unless klass - klass.find_by(klass.primary_key => id).try(:exclusively_update_reactive_cache!, *args) + klass + .reactive_cache_worker_finder + .call(id, *args) + .try(:exclusively_update_reactive_cache!, *args) end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/changelogs/unreleased/58375-reactive-caching-changes.yml b/changelogs/unreleased/58375-reactive-caching-changes.yml new file mode 100644 index 00000000000..cf73736b8ef --- /dev/null +++ b/changelogs/unreleased/58375-reactive-caching-changes.yml @@ -0,0 +1,5 @@ +--- +title: Allow reactive caching to be used in services +merge_request: 26839 +author: +type: added diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 32e13d5abed..53df9e0bc05 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -16,6 +16,10 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do attr_reader :id + def self.primary_key + :id + end + def initialize(id, &blk) @id = id @calculator = blk @@ -106,6 +110,46 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end end + describe '.reactive_cache_worker_finder' do + context 'with default reactive_cache_worker_finder' do + let(:args) { %w(other args) } + + before do + allow(instance.class).to receive(:find_by).with(id: instance.id) + .and_return(instance) + end + + it 'calls the activerecord find_by method' do + result = instance.class.reactive_cache_worker_finder.call(instance.id, *args) + + expect(result).to eq(instance) + expect(instance.class).to have_received(:find_by).with(id: instance.id) + end + end + + context 'with custom reactive_cache_worker_finder' do + let(:args) { %w(arg1 arg2) } + let(:instance) { CustomFinderCacheTest.new(666, &calculation) } + + class CustomFinderCacheTest < CacheTest + self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) } + + def self.from_cache(*args); end + end + + before do + allow(instance.class).to receive(:from_cache).with(*args).and_return(instance) + end + + it 'overrides the default reactive_cache_worker_finder' do + result = instance.class.reactive_cache_worker_finder.call(instance.id, *args) + + expect(result).to eq(instance) + expect(instance.class).to have_received(:from_cache).with(*args) + end + end + end + describe '#clear_reactive_cache!' do before do stub_reactive_cache(instance, 4) -- cgit v1.2.1