From fe5aca0b7106363601ed5f4aefa032e0564bfb10 Mon Sep 17 00:00:00 2001
From: Thea Choem <29684683+theachoem@users.noreply.github.com>
Date: Wed, 20 Nov 2024 10:00:04 +0700
Subject: [PATCH] close #2058 invalidate cache for frequently use API (#2059)
---
.env.example | 6 ++-
.../spree_cm_commissioner/content_cachable.rb | 20 ++++++++
.../spree/admin/base_controller_decorator.rb | 23 +++++++++
.../action_controller/api_decorator.rb | 1 +
.../application_controller_decorator.rb | 1 +
.../invalidate_cache_request.rb | 31 +++++++++++
.../invalidate_cache_request_job.rb | 7 +++
.../admin/homepage_section/index.html.erb | 1 +
config/routes.rb | 2 +
...ed_to_invalidate_cache_return_response.yml | 51 +++++++++++++++++++
.../invalidate_cache_request_spec.rb | 20 ++++++++
.../invalidate_cache_request_job_spec.rb | 10 ++++
12 files changed, 172 insertions(+), 1 deletion(-)
create mode 100644 app/controllers/concerns/spree_cm_commissioner/content_cachable.rb
create mode 100644 app/interactors/spree_cm_commissioner/invalidate_cache_request.rb
create mode 100644 app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb
create mode 100644 spec/cassettes/SpreeCmCommissioner_InvalidateCacheRequest/_call/requested_to_invalidate_cache_return_response.yml
create mode 100644 spec/interactors/spree_cm_commissioner/invalidate_cache_request_spec.rb
create mode 100644 spec/jobs/spree_cm_commissioner/invalidate_cache_request_job_spec.rb
diff --git a/.env.example b/.env.example
index e425f4df8..9f67ccdf3 100644
--- a/.env.example
+++ b/.env.example
@@ -3,4 +3,8 @@ AWS_CF_PUBLIC_KEY_ID="KKC****QFJ2"
AWS_OUTPUT_BUCKET_NAME="output-production-cm"
AWS_CF_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
------END PRIVATE KEY-----"
\ No newline at end of file
+-----END PRIVATE KEY-----"
+
+# for GET request with following host, it will be cached.
+CONTENT_HOST_URL=https://content.bookme.plus
+CONTENT_CACHE_MAX_AGE=7200
diff --git a/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb b/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb
new file mode 100644
index 000000000..73f362d84
--- /dev/null
+++ b/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb
@@ -0,0 +1,20 @@
+module SpreeCmCommissioner
+ module ContentCachable
+ extend ActiveSupport::Concern
+
+ included do
+ after_action :set_cache_control_for_cdn
+ end
+
+ def max_age
+ ENV.fetch('CONTENT_CACHE_MAX_AGE', '7200')
+ end
+
+ def set_cache_control_for_cdn
+ return unless request.get? || request.head?
+ return unless request.base_url == ENV['CONTENT_HOST_URL']
+
+ response.set_header('Cache-Control', "public, max-age=#{max_age}")
+ end
+ end
+end
diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb
index d36828219..7e12ca77a 100644
--- a/app/controllers/spree/admin/base_controller_decorator.rb
+++ b/app/controllers/spree/admin/base_controller_decorator.rb
@@ -31,6 +31,29 @@ def parse_date(date, format: nil)
nil
end
+ # POST
+ def invalidate_api_caches
+ if params[:model].present?
+ api_patterns_map = {
+ 'SpreeCmCommissioner::HomepageSection' => '/api/v2/storefront/homepage/*',
+ 'SpreeCmCommissioner::HomepageBackground' => '/api/v2/storefront/homepage/*',
+ 'Spree::Menu' => '/api/v2/storefront/menus*'
+ }
+
+ api_patterns = api_patterns_map[params[:model]]
+
+ if api_patterns.is_a?(Array)
+ api_patterns.each { |pattern| SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(pattern) }
+ elsif api_patterns.is_a?(String)
+ SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(api_patterns)
+ end
+ elsif params[:api_pattern].present?
+ SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(params[:api_pattern])
+ end
+
+ redirect_back fallback_location: admin_root_path
+ end
+
private
def redirect_to_billing
diff --git a/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb b/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb
index 535f40027..efdc29f09 100644
--- a/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb
+++ b/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb
@@ -3,6 +3,7 @@ module ActionController
module ApiDecorator
def self.prepended(base)
base.include SpreeCmCommissioner::ExceptionNotificable
+ base.include SpreeCmCommissioner::ContentCachable
end
# Annonymous block: https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Naming/BlockForwarding
diff --git a/app/controllers/spree_cm_commissioner/application_controller_decorator.rb b/app/controllers/spree_cm_commissioner/application_controller_decorator.rb
index 430a85f1c..be4740fa1 100644
--- a/app/controllers/spree_cm_commissioner/application_controller_decorator.rb
+++ b/app/controllers/spree_cm_commissioner/application_controller_decorator.rb
@@ -2,6 +2,7 @@ module SpreeCmCommissioner
module ApplicationControllerDecorator
def self.prepended(base)
base.include SpreeCmCommissioner::ExceptionNotificable
+ base.include SpreeCmCommissioner::ContentCachable
end
# Annonymous block: https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Naming/BlockForwarding
diff --git a/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb b/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb
new file mode 100644
index 000000000..84a1a0ddb
--- /dev/null
+++ b/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb
@@ -0,0 +1,31 @@
+require 'aws-sdk-cloudfront'
+
+# pattern: '/api/v2/storefront/homepage_sections*'
+# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/invalidation-access-logs.html
+
+module SpreeCmCommissioner
+ class InvalidateCacheRequest < BaseInteractor
+ delegate :pattern, to: :context
+
+ def call
+ client = ::Aws::CloudFront::Client.new(
+ region: ENV.fetch('AWS_REGION'),
+ access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
+ secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY'),
+ http_open_timeout: 15,
+ http_read_timeout: 60
+ )
+
+ context.response = client.create_invalidation(
+ distribution_id: ENV.fetch('ASSETS_SYNC_CF_DIST_ID'),
+ invalidation_batch: {
+ caller_reference: Time.now.to_i.to_s,
+ paths: {
+ quantity: 1,
+ items: [pattern]
+ }
+ }
+ )
+ end
+ end
+end
diff --git a/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb b/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb
new file mode 100644
index 000000000..e1c7ac26d
--- /dev/null
+++ b/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb
@@ -0,0 +1,7 @@
+module SpreeCmCommissioner
+ class InvalidateCacheRequestJob < ApplicationJob
+ def perform(pattern)
+ SpreeCmCommissioner::InvalidateCacheRequest.call(pattern: pattern)
+ end
+ end
+end
diff --git a/app/views/spree/admin/homepage_section/index.html.erb b/app/views/spree/admin/homepage_section/index.html.erb
index bfa2da64d..2f7e9d32f 100644
--- a/app/views/spree/admin/homepage_section/index.html.erb
+++ b/app/views/spree/admin/homepage_section/index.html.erb
@@ -3,6 +3,7 @@
<% end %>
<% content_for :page_actions do %>
+ <%= button_link_to Spree.t(:clear_cache), admin_invalidate_api_caches_path(model: SpreeCmCommissioner::HomepageSection.name), method: :post, class: "btn btn-outline-primary" %>
<%= button_link_to Spree.t(:new_homepage_section), new_admin_homepage_feed_homepage_section_path, { class: "btn-success", icon: 'add.svg', id: 'admin_new_homepage_section' } %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index f5f0ae94a..4a44c662b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,6 +1,8 @@
Spree::Core::Engine.add_routes do
# Add your extension routes here
namespace :admin do
+ post '/invalidate_api_caches', to: 'base#invalidate_api_caches'
+
resources :promotions do
resources :custom_dates_rules, controller: :promotion_custom_dates_rules, only: %i[edit update] do
member do
diff --git a/spec/cassettes/SpreeCmCommissioner_InvalidateCacheRequest/_call/requested_to_invalidate_cache_return_response.yml b/spec/cassettes/SpreeCmCommissioner_InvalidateCacheRequest/_call/requested_to_invalidate_cache_return_response.yml
new file mode 100644
index 000000000..8f4fba04d
--- /dev/null
+++ b/spec/cassettes/SpreeCmCommissioner_InvalidateCacheRequest/_call/requested_to_invalidate_cache_return_response.yml
@@ -0,0 +1,51 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://cloudfront.amazonaws.com/2020-05-31/distribution/D12FAKEFAKE/invalidation
+ body:
+ encoding: UTF-8
+ string: 1/staging/422.html1729834869
+ headers:
+ Accept-Encoding:
+ - ''
+ Content-Type:
+ - application/xml
+ User-Agent:
+ - aws-sdk-ruby3/3.209.1 ua/2.1 api/cloudfront#1.82.0 os/macos#23 md/arm64 lang/ruby#3.2.0
+ md/3.2.0 m/D
+ Host:
+ - cloudfront.amazonaws.com
+ X-Amz-Date:
+ - 20241025T054109Z
+ X-Amz-Content-Sha256:
+ - 4231c5527e8a773761d8806a1578ab2d799479bf369bd60e0f5a4af0f08afa9b
+ Authorization:
+ - AWS4-HMAC-SHA256 Credential=AXXXXXXXXXXXXXXXY/20241025/us-east-1/cloudfront/aws4_request,
+ SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=66b25acc5a915f366e1da91e6ab214dfe1eb55c183fb2196836d6df212c696eb
+ Content-Length:
+ - '222'
+ Accept:
+ - "*/*"
+ response:
+ status:
+ code: 201
+ message: Created
+ headers:
+ X-Amzn-Requestid:
+ - 4dc45b65-cabf-4d0a-8253-7955e851cacd
+ Location:
+ - https://cloudfront.amazonaws.com/2020-05-31/distribution/D12FAKEFAKE/invalidation/I2PGO1F2LAWEKXXYAKMN0P38N1
+ Content-Type:
+ - text/xml
+ Content-Length:
+ - '384'
+ Date:
+ - Fri, 25 Oct 2024 05:41:10 GMT
+ body:
+ encoding: UTF-8
+ string: |-
+
+ I2PGO1F2LAWEKXXYAKMN0P38N1InProgress2024-10-25T05:41:11.383Z1/staging/422.html1729834869
+ recorded_at: Fri, 25 Oct 2024 05:41:11 GMT
+recorded_with: VCR 6.1.0
diff --git a/spec/interactors/spree_cm_commissioner/invalidate_cache_request_spec.rb b/spec/interactors/spree_cm_commissioner/invalidate_cache_request_spec.rb
new file mode 100644
index 000000000..861cc2670
--- /dev/null
+++ b/spec/interactors/spree_cm_commissioner/invalidate_cache_request_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+RSpec.describe SpreeCmCommissioner::InvalidateCacheRequest do
+ before do
+ ENV['AWS_REGION'] = 'ap-southeast-1'
+ ENV['AWS_ACCESS_KEY_ID'] = 'AXXXXXXXXXXXXXXXY'
+ ENV['AWS_SECRET_ACCESS_KEY'] = 'VO103FAKEFAKEFAKE3020X=I230x1'
+ ENV['ASSETS_SYNC_CF_DIST_ID'] = 'D12FAKEFAKE'
+ end
+
+ describe '.call', :vcr do
+ it 'requested to invalidate cache & return response' do
+ context = described_class.call(pattern: '/staging/422.html')
+
+ expect(context.response.location).to eq "https://cloudfront.amazonaws.com/2020-05-31/distribution/D12FAKEFAKE/invalidation/I2PGO1F2LAWEKXXYAKMN0P38N1"
+ expect(context.response.invalidation.id).to eq "I2PGO1F2LAWEKXXYAKMN0P38N1"
+ expect(context.response.invalidation.status).to eq "InProgress"
+ end
+ end
+end
diff --git a/spec/jobs/spree_cm_commissioner/invalidate_cache_request_job_spec.rb b/spec/jobs/spree_cm_commissioner/invalidate_cache_request_job_spec.rb
new file mode 100644
index 000000000..f01a7b128
--- /dev/null
+++ b/spec/jobs/spree_cm_commissioner/invalidate_cache_request_job_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+RSpec.describe SpreeCmCommissioner::InvalidateCacheRequestJob do
+ describe '#perform' do
+ it 'invokes StateUpdater.call' do
+ expect(SpreeCmCommissioner::InvalidateCacheRequest).to receive(:call).with(pattern: '/api/storefront/*')
+ described_class.new.perform('/api/storefront/*')
+ end
+ end
+end