mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-30 21:27:17 +00:00
Merge pull request #5318 from Matt-Yorkley/caching-api
[Caching] API Action Caching on shop filters
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
module Api
|
||||
class OrderCyclesController < Api::BaseController
|
||||
include EnterprisesHelper
|
||||
respond_to :json
|
||||
include ApiActionCaching
|
||||
|
||||
skip_authorization_check
|
||||
skip_before_filter :authenticate_user, :ensure_api_key, only: [:taxons, :properties]
|
||||
|
||||
caches_action :taxons, :properties,
|
||||
expires_in: CacheService::FILTERS_EXPIRY,
|
||||
cache_path: proc { |controller| controller.request.url }
|
||||
|
||||
def products
|
||||
render_no_products unless order_cycle.open?
|
||||
|
||||
|
||||
21
app/models/concerns/api_action_caching.rb
Normal file
21
app/models/concerns/api_action_caching.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API controllers inherit from ActionController::Metal to keep them slim and fast.
|
||||
# This concern adds the minimum requirements needed to use Action Caching in the API.
|
||||
|
||||
module ApiActionCaching
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ActionController::Caching
|
||||
include ActionController::Caching::Actions
|
||||
include AbstractController::Layouts
|
||||
|
||||
# These configs are not assigned to the controller automatically with ActionController::Metal
|
||||
self.cache_store = Rails.configuration.cache_store
|
||||
self.perform_caching = true
|
||||
|
||||
# ActionController::Caching asks for a controller's layout, but they're not used in the API
|
||||
layout false
|
||||
end
|
||||
end
|
||||
25
app/services/cache_service.rb
Normal file
25
app/services/cache_service.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CacheService
|
||||
FILTERS_EXPIRY = 30.seconds.freeze
|
||||
|
||||
def self.cache(cache_key, options = {})
|
||||
Rails.cache.fetch cache_key.to_s, options do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Yields a cached query, expired by the most recently updated record for a given class.
|
||||
# E.g: if *any* Spree::Taxon record is updated, all keys based on Spree::Taxon will auto-expire.
|
||||
def self.cached_data_by_class(cache_key, cached_class)
|
||||
Rails.cache.fetch "#{cache_key}-#{cached_class}-#{latest_timestamp_by_class(cached_class)}" do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Gets the :updated_at value of the most recently updated record for a given class, and returns
|
||||
# it as a timestamp, eg: `1583836069`.
|
||||
def self.latest_timestamp_by_class(cached_class)
|
||||
cached_class.maximum(:updated_at).to_i
|
||||
end
|
||||
end
|
||||
68
spec/features/consumer/caching/shops_caching_spec.rb
Normal file
68
spec/features/consumer/caching/shops_caching_spec.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
feature "Shops caching", js: true, caching: true do
|
||||
include WebHelper
|
||||
include UIComponentHelper
|
||||
|
||||
let!(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, is_primary_producer: true) }
|
||||
let!(:order_cycle) { create(:open_order_cycle, distributors: [distributor], coordinator: distributor) }
|
||||
|
||||
describe "API action caching on taxons and properties" do
|
||||
let!(:taxon) { create(:taxon, name: "Cached Taxon") }
|
||||
let!(:taxon2) { create(:taxon, name: "New Taxon") }
|
||||
let!(:property) { create(:property, presentation: "Cached Property") }
|
||||
let!(:property2) { create(:property, presentation: "New Property") }
|
||||
let!(:product) { create(:product, taxons: [taxon], primary_taxon: taxon, properties: [property]) }
|
||||
let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first }
|
||||
|
||||
let(:test_domain) { "#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}" }
|
||||
let(:taxons_key) { "views/#{test_domain}/api/order_cycles/#{order_cycle.id}/taxons?distributor=#{distributor.id}" }
|
||||
let(:properties_key) { "views/#{test_domain}/api/order_cycles/#{order_cycle.id}/properties?distributor=#{distributor.id}" }
|
||||
let(:options) { { expires_in: CacheService::FILTERS_EXPIRY } }
|
||||
|
||||
before do
|
||||
exchange.variants << product.variants.first
|
||||
end
|
||||
|
||||
it "caches rendered response for taxons and properties, with the provided options" do
|
||||
visit enterprise_shop_path(distributor)
|
||||
|
||||
expect(page).to have_content "Cached Taxon"
|
||||
expect(page).to have_content "Cached Property"
|
||||
|
||||
expect_cached taxons_key, options
|
||||
expect_cached properties_key, options
|
||||
end
|
||||
|
||||
it "keeps data cached for a short time on subsequent requests" do
|
||||
# One minute ago...
|
||||
Timecop.travel(Time.zone.now - 1.minute) do
|
||||
visit enterprise_shop_path(distributor)
|
||||
|
||||
expect(page).to have_content taxon.name
|
||||
expect(page).to have_content property.presentation
|
||||
|
||||
product.update_attribute(:taxons, [taxon2])
|
||||
product.update_attribute(:primary_taxon, taxon2)
|
||||
product.update_attribute(:properties, [property2])
|
||||
|
||||
visit enterprise_shop_path(distributor)
|
||||
|
||||
expect(page).to have_content taxon.name # Taxon list is unchanged
|
||||
expect(page).to have_content property.presentation # Property list is unchanged
|
||||
end
|
||||
|
||||
# A while later...
|
||||
visit enterprise_shop_path(distributor)
|
||||
|
||||
expect(page).to have_content taxon2.name
|
||||
expect(page).to have_content property2.presentation
|
||||
end
|
||||
end
|
||||
|
||||
def expect_cached(key, options = {})
|
||||
expect(Rails.cache.exist?(key, options)).to be true
|
||||
end
|
||||
end
|
||||
50
spec/services/cache_service_spec.rb
Normal file
50
spec/services/cache_service_spec.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe CacheService do
|
||||
let(:rails_cache) { Rails.cache }
|
||||
|
||||
describe "#cache" do
|
||||
before do
|
||||
rails_cache.stub(:fetch)
|
||||
end
|
||||
|
||||
it "provides a wrapper for basic #fetch calls to Rails.cache" do
|
||||
CacheService.cache("test-cache-key", expires_in: 10.seconds) do
|
||||
"TEST"
|
||||
end
|
||||
|
||||
expect(rails_cache).to have_received(:fetch).with("test-cache-key", expires_in: 10.seconds)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#cached_data_by_class" do
|
||||
let(:timestamp) { Time.now.to_i }
|
||||
|
||||
before do
|
||||
rails_cache.stub(:fetch)
|
||||
CacheService.stub(:latest_timestamp_by_class) { timestamp }
|
||||
end
|
||||
|
||||
it "caches data by timestamp for last record of that class" do
|
||||
CacheService.cached_data_by_class("test-cache-key", Enterprise) do
|
||||
"TEST"
|
||||
end
|
||||
|
||||
expect(CacheService).to have_received(:latest_timestamp_by_class).with(Enterprise)
|
||||
expect(rails_cache).to have_received(:fetch).with("test-cache-key-Enterprise-#{timestamp}")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#latest_timestamp_by_class" do
|
||||
let!(:taxon1) { create(:taxon) }
|
||||
let!(:taxon2) { create(:taxon) }
|
||||
|
||||
it "gets the :updated_at value of the last record for a given class and returns a timestamp" do
|
||||
taxon1.touch
|
||||
expect(CacheService.latest_timestamp_by_class(Spree::Taxon)).to eq taxon1.updated_at.to_i
|
||||
|
||||
taxon2.touch
|
||||
expect(CacheService.latest_timestamp_by_class(Spree::Taxon)).to eq taxon2.updated_at.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -114,6 +114,15 @@ RSpec.configure do |config|
|
||||
.each { |s| s.driver.reset! }
|
||||
end
|
||||
|
||||
# Enable caching in any specs tagged with `caching: true`. Usage is exactly the same as the
|
||||
# well-known `js: true` tag used to enable javascript in feature specs.
|
||||
config.around(:each, :caching) do |example|
|
||||
caching = ActionController::Base.perform_caching
|
||||
ActionController::Base.perform_caching = example.metadata[:caching]
|
||||
example.run
|
||||
ActionController::Base.perform_caching = caching
|
||||
end
|
||||
|
||||
config.before(:all) { restart_phantomjs }
|
||||
|
||||
# Geocoding
|
||||
|
||||
Reference in New Issue
Block a user