diff --git a/app/services/edge_cache/bust.rb b/app/services/edge_cache/bust.rb index 963773b51..d40f66a56 100644 --- a/app/services/edge_cache/bust.rb +++ b/app/services/edge_cache/bust.rb @@ -1,36 +1,27 @@ module EdgeCache class Bust - PROVIDERS = %w[fastly nginx].freeze - def self.call(*args) new(*args).call end def initialize(path) - # TODO: (Vaidehi Joshi) - Right now, we are checking that nginx is - # available on every purge request/call to this bust service. If we are going - # to bust multiple paths, we should be able to check that nginx is - # available just once, and persist it on the class with @provider_available?. - # Then, we could allow for an array of @paths = [] to be passed in, - # and on single bust instance could bust multiple paths in order. - @path = path @provider = determine_provider @response = nil end def call - return unless PROVIDERS.include?(provider) + return unless provider - bust_method = "bust_#{provider}_cache" - if respond_to?(bust_method, true) - @response = __send__(bust_method) + provider_class = "#{self.class}::#{provider.capitalize}".constantize + + if provider_class.respond_to?(:call) + @response = provider_class.call(path) else - # We theoretically should never hit this unless someone adds a provider - # but doesn't add the implementation for it into the #call method. @response = nil - Rails.logger.warn("EdgeCache::Bust was called with an invalid provider: #{provider}") - DatadogStatsClient.increment("edgecache_bust.invalid_provider", tags: ["provider:#{provider}"]) + Rails.logger.warn("#{provider_class} cannot be used without a #call implementation!") + DatadogStatsClient.increment("edgecache_bust.invalid_provider_class", + tags: ["provider_class:#{provider_class}"]) end self @@ -43,40 +34,11 @@ module EdgeCache def determine_provider if fastly_enabled? "fastly" - elsif nginx_enabled? && nginx_available? + elsif nginx_enabled? "nginx" end end - def bust_fastly_cache - # TODO: (Alex Smith) - It would be "nice to have" the ability to use the - # Fastly gem here instead of custom API calls. - - # @forem/systems Fastly-enabled forems don't need "flexible" domains. - HTTParty.post( - "https://api.fastly.com/purge/https://#{URL.domain}#{path}", - headers: { - "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] - }, - ) - HTTParty.post( - "https://api.fastly.com/purge/https://#{URL.domain}#{path}?i=i", - headers: { - "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] - }, - ) - end - - def bust_nginx_cache - uri = URI.parse("#{openresty_path}#{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 fastly_enabled? ApplicationConfig["FASTLY_API_KEY"].present? && ApplicationConfig["FASTLY_SERVICE_ID"].present? end @@ -84,32 +46,5 @@ module EdgeCache def nginx_enabled? ApplicationConfig["OPENRESTY_PROTOCOL"].present? && ApplicationConfig["OPENRESTY_DOMAIN"].present? end - - def openresty_path - "#{ApplicationConfig['OPENRESTY_PROTOCOL']}#{ApplicationConfig['OPENRESTY_DOMAIN']}" - end - - def nginx_available? - uri = URI.parse(openresty_path) - http = Net::HTTP.new(uri.host, uri.port) - response = http.get(uri.request_uri) - - return true if response.is_a?(Net::HTTPSuccess) - rescue StandardError - # If we can't connect to Openresty, alert ourselves that - # it is unavailable and return false. - Rails.logger.error("Could not connect to Openresty via #{openresty_path}!") - DatadogStatsClient.increment("edgecache_bust.service_unavailable", tags: ["path:#{openresty_path}"]) - false - 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/services/edge_cache/bust/fastly.rb b/app/services/edge_cache/bust/fastly.rb new file mode 100644 index 000000000..ee8e03e9a --- /dev/null +++ b/app/services/edge_cache/bust/fastly.rb @@ -0,0 +1,24 @@ +module EdgeCache + class Bust + class Fastly + def self.call(path) + # TODO: (Alex Smith) - It would be "nice to have" the ability to use the + # Fastly gem here instead of custom API calls. + + # @forem/systems Fastly-enabled forems don't need "flexible" domains. + HTTParty.post( + "https://api.fastly.com/purge/https://#{URL.domain}#{path}", + headers: { + "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] + }, + ) + HTTParty.post( + "https://api.fastly.com/purge/https://#{URL.domain}#{path}?i=i", + headers: { + "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] + }, + ) + end + end + end +end diff --git a/app/services/edge_cache/bust/nginx.rb b/app/services/edge_cache/bust/nginx.rb new file mode 100644 index 000000000..e13f00d46 --- /dev/null +++ b/app/services/edge_cache/bust/nginx.rb @@ -0,0 +1,50 @@ +module EdgeCache + class Bust + class Nginx + def self.call(path) + return unless nginx_available? + + uri = URI.parse("#{openresty_path}#{path}") + http = Net::HTTP.new(uri.host, uri.port) + response = http.request Net::HTTP::Purge.new(uri.request_uri) + + raise StandardError, "Nginx Purge request failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess) + + response.body + end + + def self.openresty_path + "#{ApplicationConfig['OPENRESTY_PROTOCOL']}#{ApplicationConfig['OPENRESTY_DOMAIN']}" + end + + def self.nginx_available? + # TODO: (Vaidehi Joshi) - Right now, we are checking that nginx is + # available on every purge request/call to this bust service. If we are going + # to bust multiple paths, we should be able to check that nginx is + # available just once, and persist it on the class with @provider_available?. + # Then, we could allow for an array of @paths = [] to be passed in, + # and on single bust instance could bust multiple paths in order. + uri = URI.parse(openresty_path) + http = Net::HTTP.new(uri.host, uri.port) + response = http.get(uri.request_uri) + + return true if response.is_a?(Net::HTTPSuccess) + rescue StandardError + # If we can't connect to Openresty, alert ourselves that + # it is unavailable and return false. + Rails.logger.error("Could not connect to Openresty via #{openresty_path}!") + DatadogStatsClient.increment("edgecache_bust.service_unavailable", tags: ["path:#{openresty_path}"]) + false + end + 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::Purge < Net::HTTPRequest # rubocop:disable Style/ClassAndModuleChildren + METHOD = "PURGE".freeze + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end diff --git a/spec/services/edge_cache/bust_spec.rb b/spec/services/edge_cache/bust_spec.rb index 3fc2fb854..c3f5bfc81 100644 --- a/spec/services/edge_cache/bust_spec.rb +++ b/spec/services/edge_cache/bust_spec.rb @@ -5,6 +5,8 @@ RSpec.describe EdgeCache::Bust, type: :service do let(:path) { "/#{user.username}" } describe "#bust_fastly_cache" do + let(:fastly_provider_class) { EdgeCache::Bust::Fastly } + context "when fastly is not configured" do before do stub_fastly @@ -14,11 +16,11 @@ RSpec.describe EdgeCache::Bust, type: :service do let(:cache_bust_service) { described_class.new(path) } it "does not bust a fastly cache" do - allow(cache_bust_service).to receive(:bust_fastly_cache) + allow(fastly_provider_class).to receive(:call) cache_bust_service.call expect(cache_bust_service.provider).to be(nil) - expect(cache_bust_service).not_to have_received(:bust_fastly_cache) + expect(fastly_provider_class).not_to have_received(:call) end end @@ -31,15 +33,15 @@ RSpec.describe EdgeCache::Bust, type: :service do let(:cache_bust_service) { described_class.new(path) } it "can bust a fastly cache" do - allow(cache_bust_service).to receive(:bust_fastly_cache) + allow(fastly_provider_class).to receive(:call) cache_bust_service.call expect(cache_bust_service.provider).to eq("fastly") - expect(cache_bust_service).to have_received(:bust_fastly_cache) + expect(fastly_provider_class).to have_received(:call) end it "returns cache bust response" do - allow(cache_bust_service).to receive(:bust_fastly_cache).and_return("success") + allow(fastly_provider_class).to receive(:call).and_return("success") cache_bust_service.call expect(cache_bust_service.response).to eq("success") @@ -48,6 +50,8 @@ RSpec.describe EdgeCache::Bust, type: :service do end describe "#bust_nginx_cache" do + let(:nginx_provider_class) { EdgeCache::Bust::Nginx } + before do # Explicitly stub Fastly since we check if Fastly has # been configured before we try to use Nginx. @@ -62,11 +66,11 @@ RSpec.describe EdgeCache::Bust, type: :service do let(:cache_bust_service) { described_class.new(path) } it "does not bust an nginx cache" do - allow(cache_bust_service).to receive(:bust_nginx_cache) + allow(nginx_provider_class).to receive(:call) cache_bust_service.call expect(cache_bust_service.provider).to eq(nil) - expect(cache_bust_service).not_to have_received(:bust_nginx_cache) + expect(nginx_provider_class).not_to have_received(:call) end end @@ -78,11 +82,11 @@ RSpec.describe EdgeCache::Bust, type: :service do let(:cache_bust_service) { described_class.new(path) } it "can bust an nginx cache" do - allow(cache_bust_service).to receive(:bust_nginx_cache) + allow(nginx_provider_class).to receive(:call) cache_bust_service.call expect(cache_bust_service.provider).to eq("nginx") - expect(cache_bust_service).to have_received(:bust_nginx_cache) + expect(nginx_provider_class).to have_received(:call) end end end