diff --git a/.env_sample b/.env_sample index a1f7a124b..19eb5fa3b 100644 --- a/.env_sample +++ b/.env_sample @@ -2,6 +2,10 @@ export APP_DOMAIN="localhost:3000" export APP_PROTOCOL="http://" +# Openresty domain + Protocol setting for development +export OPENRESTY_DOMAIN="" +export OPENRESTY_PROTOCOL="" + # Community Related Variables export COMMUNITY_NAME="DEV(local)" diff --git a/app/labor/cache_buster.rb b/app/labor/cache_buster.rb index 360d523ed..6cba24fdc 100644 --- a/app/labor/cache_buster.rb +++ b/app/labor/cache_buster.rb @@ -18,18 +18,49 @@ module CacheBuster # fastly.purge(path) # # https://github.com/fastly/fastly-ruby#efficient-purging - return unless Rails.env.production? - return if ApplicationConfig["FASTLY_API_KEY"].blank? - - HTTParty.post("https://api.fastly.com/purge/https://#{ApplicationConfig['APP_DOMAIN']}#{path}", - headers: { "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] }) - HTTParty.post("https://api.fastly.com/purge/https://#{ApplicationConfig['APP_DOMAIN']}#{path}?i=i", - headers: { "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] }) + if fastly_enabled? + bust_fastly_cache(path) + elsif nginx_enabled? + bust_nginx_cache(path) + end rescue URI::InvalidURIError => e Rails.logger.error("Trying to bust cache of an invalid uri: #{e}") DatadogStatsClient.increment("cache_buster.invalid_uri", tags: ["path:#{path}"]) end + def self.fastly_enabled? + ApplicationConfig["FASTLY_API_KEY"].present? && ApplicationConfig["FASTLY_SERVICE_ID"].present? + end + + def self.nginx_enabled? + ApplicationConfig["OPENRESTY_PROTOCOL"].present? && ApplicationConfig["OPENRESTY_DOMAIN"].present? + end + + def self.bust_fastly_cache(path) + HTTParty.post( + "https://api.fastly.com/purge/https://#{ApplicationConfig['APP_DOMAIN']}#{path}", + headers: { + "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] + }, + ) + HTTParty.post( + "https://api.fastly.com/purge/https://#{ApplicationConfig['APP_DOMAIN']}#{path}?i=i", + headers: { + "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] + }, + ) + end + + def self.bust_nginx_cache(path) + uri = URI.parse("#{ApplicationConfig['OPENRESTY_PROTOCOL']}#{ApplicationConfig['OPENRESTY_DOMAIN']}#{path}") + http = Net::HTTP.new(uri.host, uri.port) + response = http.request Net::HTTP::NginxPurge.new(uri.request_uri) + + raise StandardError, "NginxPurge request failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess) + + response.body + end + def self.bust_comment(commentable) return unless commentable @@ -206,3 +237,12 @@ module CacheBuster end end end + +# Creates our own purge method for an HTTP request, +# which is used by Nginx to bust a cache. +# See Net::HTTPGenericRequest for attributes/methods. +class Net::HTTP::NginxPurge < Net::HTTPRequest # rubocop:disable Style/ClassAndModuleChildren + METHOD = "PURGE".freeze + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end diff --git a/app/models/article.rb b/app/models/article.rb index bc468834b..0180eab8f 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -645,8 +645,6 @@ class Article < ApplicationRecord end def bust_cache - return unless Rails.env.production? - CacheBuster.bust(path) CacheBuster.bust("#{path}?i=i") CacheBuster.bust("#{path}?preview=#{password}") diff --git a/app/models/concerns/purgeable.rb b/app/models/concerns/purgeable.rb index ea7c7eec9..d103916e9 100644 --- a/app/models/concerns/purgeable.rb +++ b/app/models/concerns/purgeable.rb @@ -1,19 +1,20 @@ # Copied from the deprecated fastly-rails gem # https://github.com/fastly/fastly-rails/blob/master/lib/fastly-rails/active_record/surrogate_key.rb # -# This concern handles purge and purge_all calls to purge the edge cache (Fastly) +# This concern handles purge and purge_all calls to purge Fastly's edge cache. +# If Fastly has not been configured, these methods will short circuit and not be invoked. module Purgeable extend ActiveSupport::Concern module ClassMethods def purge_all - return if Rails.env.development? + return unless fastly service.purge_by_key(table_key) end def soft_purge_all - return if Rails.env.development? + return unless fastly service.purge_by_key(table_key, true) end @@ -23,13 +24,14 @@ module Purgeable end def fastly - return if Rails.env.development? + return false if Rails.env.development? + return false if ApplicationConfig["FASTLY_API_KEY"].blank? || ApplicationConfig["FASTLY_SERVICE_ID"].blank? Fastly.new(api_key: ApplicationConfig["FASTLY_API_KEY"]) end def service - return if Rails.env.development? + return unless fastly Fastly::Service.new({ id: ApplicationConfig["FASTLY_SERVICE_ID"] }, fastly) end @@ -45,13 +47,13 @@ module Purgeable end def purge - return if Rails.env.development? + return unless fastly service.purge_by_key(record_key) end def soft_purge - return if Rails.env.development? + return unless fastly service.purge_by_key(record_key, true) end diff --git a/spec/labor/cache_buster_spec.rb b/spec/labor/cache_buster_spec.rb index 1d349e8af..0e73a5e33 100644 --- a/spec/labor/cache_buster_spec.rb +++ b/spec/labor/cache_buster_spec.rb @@ -11,6 +11,31 @@ RSpec.describe CacheBuster, type: :labor do let(:podcast_episode) { create(:podcast_episode, podcast_id: podcast.id) } let(:tag) { create(:tag) } + describe "#bust_nginx_cache" do + before do + allow(cache_buster).to receive(:bust_nginx_cache).and_call_original + allow(ApplicationConfig).to receive(:[]).with("OPENRESTY_PROTOCOL").and_return("http://") + allow(ApplicationConfig).to receive(:[]).with("OPENRESTY_DOMAIN").and_return("localhost:9090") + end + + it "can bust an nginx cache when configured" do + cache_buster.bust_nginx_cache("/#{user.username}") + end + end + + describe "#bust_fastly_cache" do + before do + allow(cache_buster).to receive(:bust_fastly_cache).and_call_original + allow(ApplicationConfig).to receive(:[]).with("APP_DOMAIN").and_return("fake-key") + allow(ApplicationConfig).to receive(:[]).with("FASTLY_API_KEY").and_return("fake-key") + allow(ApplicationConfig).to receive(:[]).with("FASTLY_SERVICE_ID").and_return("localhost:3000") + end + + it "can bust a fastly cache when configured" do + cache_buster.bust_fastly_cache("/#{user.username}") + end + end + describe "#bust_comment" do it "busts comment" do cache_buster.bust_comment(comment.commentable) diff --git a/spec/models/concerns/purgeable_spec.rb b/spec/models/concerns/purgeable_spec.rb index 5436aa56b..20046b9d2 100644 --- a/spec/models/concerns/purgeable_spec.rb +++ b/spec/models/concerns/purgeable_spec.rb @@ -18,6 +18,8 @@ RSpec.describe Purgeable do let(:fastly_service) { instance_double(Fastly::Service) } before do + allow(ApplicationConfig).to receive(:[]).with("FASTLY_API_KEY").and_return("fake-key") + allow(ApplicationConfig).to receive(:[]).with("FASTLY_SERVICE_ID").and_return("fake-service-id") allow(Fastly::Service).to receive(:new).and_return(fastly_service) end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 9bf304707..6b5b0324b 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -139,6 +139,9 @@ RSpec.configure do |config| stub_request(:post, /api.fastly.com/) .to_return(status: 200, body: "".to_json, headers: {}) + stub_request(:any, /localhost:9090/) + .to_return(status: 200, body: "OK".to_json, headers: {}) + stub_request(:post, /api.bufferapp.com/) .to_return(status: 200, body: { fake_text: "so fake" }.to_json, headers: {}) diff --git a/spec/workers/articles/bust_multiple_caches_worker_spec.rb b/spec/workers/articles/bust_multiple_caches_worker_spec.rb index 7b2879af0..1ca019d85 100644 --- a/spec/workers/articles/bust_multiple_caches_worker_spec.rb +++ b/spec/workers/articles/bust_multiple_caches_worker_spec.rb @@ -2,7 +2,9 @@ require "rails_helper" RSpec.describe Articles::BustMultipleCachesWorker, type: :worker do describe "#perform" do - let(:article) { create(:article) } + # Explicitly create article before the test is invoked, since + # creating an article will invoke CacheBuster#bust in a callback. + let!(:article) { create(:article) } let(:path) { article.path } let(:worker) { subject }