Skip to content

Commit 86a1bb1

Browse files
committed
Add InvalidateElementsCacheJob
In larger applications it might be the case that one product, variant or taxon is assigned to thousands of elements via one of our three ingredients. The current implementation does not perform very well. Using a active job that can be backgrounded and has an optimized algorithm to touch all elements, their parent elements and pages should improve performance.
1 parent 93590c9 commit 86a1bb1

11 files changed

Lines changed: 143 additions & 70 deletions

File tree

app/decorators/models/alchemy/solidus/spree_product_decorator.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ module Alchemy
44
module Solidus
55
module SpreeProductDecorator
66
def self.prepended(base)
7-
base.include Alchemy::Solidus::TouchAlchemyIngredients
7+
# Solidus runs touch callbacks on product after_save
8+
base.after_touch :touch_alchemy_ingredients
89
base.has_many :alchemy_ingredients, class_name: "Alchemy::Ingredients::SpreeProduct", as: :related_object, dependent: :nullify
910
end
1011

@@ -23,6 +24,12 @@ def touch_taxons
2324
taxons.each(&:touch)
2425
end
2526

27+
# Touch all elements that have this product assigned to one of its ingredients.
28+
# This cascades to all parent elements and pages as well.
29+
def touch_alchemy_ingredients
30+
InvalidateElementsCacheJob.perform_later("SpreeProduct", id)
31+
end
32+
2633
::Spree::Product.prepend self
2734
end
2835
end

app/decorators/models/alchemy/solidus/spree_taxon_decorator.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@ module Alchemy
44
module Solidus
55
module SpreeTaxonDecorator
66
def self.prepended(base)
7-
base.include Alchemy::Solidus::TouchAlchemyIngredients
7+
base.after_update :touch_alchemy_ingredients
8+
base.after_touch :touch_alchemy_ingredients
89
base.has_many :alchemy_ingredients, class_name: "Alchemy::Ingredients::SpreeTaxon", as: :related_object, dependent: :nullify
910
end
1011

1112
::Spree::Taxon.prepend self
13+
14+
private
15+
16+
# Touch all elements that have this taxon assigned to one of its ingredients.
17+
# This cascades to all parent elements and pages as well.
18+
def touch_alchemy_ingredients
19+
InvalidateElementsCacheJob.perform_later("SpreeTaxon", id)
20+
end
1221
end
1322
end
1423
end

app/decorators/models/alchemy/solidus/spree_variant_decorator.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@ module Alchemy
44
module Solidus
55
module SpreeVariantDecorator
66
def self.prepended(base)
7-
base.include Alchemy::Solidus::TouchAlchemyIngredients
7+
base.after_update :touch_alchemy_ingredients
8+
base.after_touch :touch_alchemy_ingredients
89
base.has_many :alchemy_ingredients, class_name: "Alchemy::Ingredients::SpreeVariant", as: :related_object, dependent: :nullify
910
end
1011

1112
::Spree::Variant.prepend self
13+
14+
private
15+
16+
# Touch all elements that have this variant assigned to one of its ingredients.
17+
# This cascades to all parent elements and pages as well.
18+
def touch_alchemy_ingredients
19+
InvalidateElementsCacheJob.perform_later("SpreeVariant", id)
20+
end
1221
end
1322
end
1423
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Alchemy
2+
module Solidus
3+
class BaseJob < ActiveJob::Base
4+
retry_on ActiveRecord::Deadlocked
5+
end
6+
end
7+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module Alchemy
2+
module Solidus
3+
class InvalidateElementsCacheJob < BaseJob
4+
queue_as :default
5+
6+
def perform(model_name, id)
7+
now = Time.current
8+
9+
element_ids = model(model_name)
10+
.where("related_object_id" => id)
11+
.joins(:element)
12+
.pluck("alchemy_elements.id")
13+
elements = ::Alchemy::Element.where(id: element_ids)
14+
15+
all_element_ids = get_all_element_ids(elements, element_ids)
16+
::Alchemy::Element.where(id: all_element_ids.uniq).update_all(updated_at: now)
17+
18+
page_ids = elements.joins(page_version: :page).pluck("alchemy_pages.id")
19+
::Alchemy::Page.where(id: page_ids.uniq).update_all(updated_at: now)
20+
end
21+
22+
private
23+
24+
def get_all_element_ids(elements, element_ids)
25+
parent_element_ids = elements.pluck(:parent_element_id).compact
26+
parent_elements = ::Alchemy::Element.distinct.where(id: parent_element_ids)
27+
28+
if parent_elements.any?
29+
element_ids += parent_element_ids
30+
get_all_element_ids(parent_elements, element_ids)
31+
end
32+
33+
element_ids
34+
end
35+
36+
def model(model_name) = "Alchemy::Ingredients::#{model_name}".constantize
37+
end
38+
end
39+
end

app/models/alchemy/solidus/touch_alchemy_ingredients.rb

Lines changed: 0 additions & 20 deletions
This file was deleted.

spec/dummy/config/environments/test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# Configure public file server for tests with Cache-Control for performance.
1919
config.public_file_server.enabled = true
2020
config.public_file_server.headers = {
21-
"Cache-Control" => "public, max-age=#{1.hour.to_i}",
21+
"Cache-Control" => "public, max-age=#{1.hour.to_i}"
2222
}
2323

2424
# Show full error reports and disable caching.
@@ -35,7 +35,7 @@
3535
# Store uploaded files on the local file system in a temporary directory.
3636
config.active_storage.service = :test
3737

38-
config.active_job.queue_adapter = :inline
38+
config.active_job.queue_adapter = :test
3939

4040
config.action_mailer.perform_caching = false
4141

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require "rails_helper"
2+
3+
RSpec.describe Alchemy::Solidus::InvalidateElementsCacheJob, type: :job do
4+
let(:job) { described_class.new }
5+
6+
subject { job.perform("SpreeProduct", product.id) }
7+
8+
let(:page) { create(:alchemy_page, :public, updated_at: 1.week.ago) }
9+
10+
let!(:element) do
11+
create(:alchemy_element, page_version: page.draft_version, ingredients: [ingredient], updated_at: 1.day.ago)
12+
end
13+
14+
describe "perform" do
15+
let(:product) { create(:product) }
16+
let(:ingredient) { Alchemy::Ingredients::SpreeProduct.create(related_object: product, role: "product") }
17+
18+
it "touches the element" do
19+
expect { subject }.to change { element.reload.updated_at }
20+
end
21+
22+
context "when the element has a parent" do
23+
let!(:parent_element) do
24+
create(:alchemy_element, page_version: page.draft_version, updated_at: 1.day.ago).tap do |parent|
25+
element.update_column(:parent_element_id, parent.id)
26+
end
27+
end
28+
29+
it "touches the parent element" do
30+
expect { subject }.to change { parent_element.reload.updated_at }
31+
end
32+
end
33+
34+
it "touches the page" do
35+
expect { subject }.to change { page.reload.updated_at }
36+
end
37+
end
38+
end

spec/models/spree/product_spec.rb

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,34 @@
1111
let(:element) { create(:alchemy_element, page_version: page_version) }
1212
let(:product) { create(:product) }
1313

14-
context "if assigned to ingredient spree taxon" do
14+
context "if assigned to ingredient spree product" do
1515
let!(:ingredient) { Alchemy::Ingredients::SpreeProduct.create!(element: element, role: "product", related_object: product) }
1616

1717
it "invalidates the cache on update" do
18-
travel_to 5.minutes.from_now do
19-
current_time = Time.current
20-
expect { product.reload.update!(name: "New name") }.to change { ingredient.reload.updated_at }.to(current_time)
21-
expect(element.reload.updated_at).to eq(current_time)
22-
expect(page_version.reload.updated_at).to eq(current_time)
23-
expect(page.reload.updated_at).to eq(current_time)
24-
end
18+
expect {
19+
product.update!(name: "New name")
20+
}.to enqueue_job(Alchemy::Solidus::InvalidateElementsCacheJob).with("SpreeProduct", product.id)
2521
end
2622

2723
it "invalidates the cache on touch" do
28-
travel_to 5.minutes.from_now do
29-
current_time = Time.current
30-
expect { product.reload.touch }.to change { ingredient.reload.updated_at }.to(current_time)
31-
expect(element.reload.updated_at).to eq(current_time)
32-
expect(page_version.reload.updated_at).to eq(current_time)
33-
expect(page.reload.updated_at).to eq(current_time)
34-
end
24+
expect {
25+
product.touch
26+
}.to enqueue_job(Alchemy::Solidus::InvalidateElementsCacheJob).with("SpreeProduct", product.id)
3527
end
3628
end
3729

3830
context "if assigned to taxon that is assigned to ingredient spree taxon" do
3931
let(:taxon) { create(:taxon) }
40-
let(:product) { create(:product, taxons: [taxon]) }
32+
let!(:product) { create(:product, taxons: [taxon]) }
4133

4234
let!(:ingredient) { Alchemy::Ingredients::SpreeTaxon.create!(element: element, role: "taxon", related_object: taxon) }
4335

4436
it "touches ingredient spree taxons elements" do
45-
expect { product.reload.touch }.to change { element.reload.updated_at }
37+
expect {
38+
product.touch
39+
}.to enqueue_job(Alchemy::Solidus::InvalidateElementsCacheJob).with("SpreeTaxon", taxon.id).and(
40+
enqueue_job(Alchemy::Solidus::InvalidateElementsCacheJob).with("SpreeProduct", product.id)
41+
)
4642
end
4743
end
4844
end

spec/models/spree/taxon_spec.rb

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,15 @@
1313
let(:taxon) { create(:taxon) }
1414

1515
it "invalidates the cache on update" do
16-
travel_to 5.minutes.from_now do
17-
current_time = Time.current
18-
expect { taxon.reload.update!(name: "New name") }.to change { ingredient.reload.updated_at }.to(current_time)
19-
expect(element.reload.updated_at).to eq(current_time)
20-
expect(page_version.reload.updated_at).to eq(current_time)
21-
expect(page.reload.updated_at).to eq(current_time)
22-
end
16+
expect {
17+
taxon.update!(name: "New name")
18+
}.to enqueue_job(Alchemy::Solidus::InvalidateElementsCacheJob).with("SpreeTaxon", taxon.id)
2319
end
2420

2521
it "invalidates the cache on touch" do
26-
travel_to 5.minutes.from_now do
27-
current_time = Time.current
28-
expect { taxon.reload.touch }.to change { ingredient.reload.updated_at }.to(current_time)
29-
expect(element.reload.updated_at).to eq(current_time)
30-
expect(page_version.reload.updated_at).to eq(current_time)
31-
expect(page.reload.updated_at).to eq(current_time)
32-
end
22+
expect {
23+
taxon.touch
24+
}.to enqueue_job(Alchemy::Solidus::InvalidateElementsCacheJob).with("SpreeTaxon", taxon.id)
3325
end
3426
end
3527
end

0 commit comments

Comments
 (0)