From 7dfffac3db65dbb9b07702300d1fcb8491d7996d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Apr 2020 10:46:15 -0400 Subject: [PATCH] [deploy] Copy ActiveRecord methods from fastly-rails (#7145) * Add comment referencing Fastly gem * Create purgable concern and include it * Typo: deprecate --> deprecated * Include Purgable on the Tag model * Rename Purgable to Purgeable * Reorganize fastly and service methods in Purgeable * Add and fix specs * Add development env guards --- app/labor/cache_buster.rb | 11 +++ app/models/application_record.rb | 2 + app/models/concerns/purgeable.rb | 74 ++++++++++++++++++ app/models/tag.rb | 3 + spec/models/concerns/purgeable_spec.rb | 101 +++++++++++++++++++++++++ spec/rails_helper.rb | 2 +- 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/purgeable.rb create mode 100644 spec/models/concerns/purgeable_spec.rb diff --git a/app/labor/cache_buster.rb b/app/labor/cache_buster.rb index 61e5dfc9a..ad6cbd434 100644 --- a/app/labor/cache_buster.rb +++ b/app/labor/cache_buster.rb @@ -7,6 +7,17 @@ module CacheBuster ].freeze def self.bust(path) + # TODO: (Alex Smith) - It would be "nice to have" the ability to use the + # Fastly gem here instead of custom API calls. We'd want to keep thread + # safety in mind. We'll also want to consider making this modular for those + # who don't want to use Fastly at all. + # + # Instead of HTTP calls, we could do: + # fastly = Fastly.new(api_key: ApplicationConfig["FASTLY_API_KEY"]) + # service = Fastly::Service.new({ id: ApplicationConfig["FASTLY_SERVICE_ID"] }, fastly) + # fastly.purge(path) + # + # https://github.com/fastly/fastly-ruby#efficient-purging return unless Rails.env.production? HTTParty.post("https://api.fastly.com/purge/https://#{ApplicationConfig['APP_DOMAIN']}#{path}", diff --git a/app/models/application_record.rb b/app/models/application_record.rb index cb35a325c..9f81fbfa9 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,6 +1,8 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + include Purgeable + QUERY_ESTIMATED_COUNT = <<~SQL.squish.freeze SELECT ( (reltuples / GREATEST(relpages, 1)) * diff --git a/app/models/concerns/purgeable.rb b/app/models/concerns/purgeable.rb new file mode 100644 index 000000000..ea7c7eec9 --- /dev/null +++ b/app/models/concerns/purgeable.rb @@ -0,0 +1,74 @@ +# 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) +module Purgeable + extend ActiveSupport::Concern + + module ClassMethods + def purge_all + return if Rails.env.development? + + service.purge_by_key(table_key) + end + + def soft_purge_all + return if Rails.env.development? + + service.purge_by_key(table_key, true) + end + + def table_key + table_name + end + + def fastly + return if Rails.env.development? + + Fastly.new(api_key: ApplicationConfig["FASTLY_API_KEY"]) + end + + def service + return if Rails.env.development? + + Fastly::Service.new({ id: ApplicationConfig["FASTLY_SERVICE_ID"] }, fastly) + end + end + + # Instance methods + def record_key + "#{table_key}/#{id}" + end + + def table_key + self.class.table_key + end + + def purge + return if Rails.env.development? + + service.purge_by_key(record_key) + end + + def soft_purge + return if Rails.env.development? + + service.purge_by_key(record_key, true) + end + + def purge_all + self.class.purge_all + end + + def soft_purge_all + self.class.soft_purge_all + end + + def fastly + self.class.fastly + end + + def service + self.class.service + end +end diff --git a/app/models/tag.rb b/app/models/tag.rb index a849b04fe..62d43c24b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -39,6 +39,9 @@ class Tag < ActsAsTaggableOn::Tag SEARCH_SERIALIZER = Search::TagSerializer SEARCH_CLASS = Search::Tag + # This model doesn't inherit from ApplicationRecord so this has to be included + include Purgeable + # possible social previews templates for articles with a particular tag def self.social_preview_templates Rails.root.join("app/views/social_previews/articles").children.map { |ch| File.basename(ch, ".html.erb") } diff --git a/spec/models/concerns/purgeable_spec.rb b/spec/models/concerns/purgeable_spec.rb new file mode 100644 index 000000000..5436aa56b --- /dev/null +++ b/spec/models/concerns/purgeable_spec.rb @@ -0,0 +1,101 @@ +require "rails_helper" + +class PurgeableModel + include Purgeable + + def self.table_name + "purgeables" + end + + def id + 1 + end +end + +RSpec.describe Purgeable do + let(:model_class) { PurgeableModel } + let(:purgeable_model) { model_class.new } + let(:fastly_service) { instance_double(Fastly::Service) } + + before do + allow(Fastly::Service).to receive(:new).and_return(fastly_service) + end + + describe "class methods" do + it "calls .purge_by_key with table_key in purge_all" do + allow(fastly_service).to receive(:purge_by_key) + model_class.purge_all + expect(fastly_service).to have_received(:purge_by_key).with(model_class.table_key) + end + + it "calls .purge_by_key with table_key and true in soft_purge_all" do + allow(fastly_service).to receive(:purge_by_key) + model_class.soft_purge_all + expect(fastly_service).to have_received(:purge_by_key).with(model_class.table_key, true) + end + + it "returns table_name with table_key" do + expect(model_class.table_key).to eq(model_class.table_name) + end + + it "returns a Fastly object" do + fastly = instance_double(Fastly) + allow(Fastly).to receive(:new).and_return(fastly) + model_class.fastly + expect(Fastly).to have_received(:new) + end + + it "returns a Fastly::Service object" do + model_class.service + expect(Fastly::Service).to have_received(:new) + end + end + + describe "instance methods" do + it "returns a record key" do + expect(purgeable_model.record_key).to eq("purgeables/1") + end + + it "returns a table_key" do + allow(model_class).to receive(:table_key).and_return(model_class.table_name) + expect(purgeable_model.table_key).to eq(model_class.table_name) + expect(model_class).to have_received(:table_key) + end + + it "calls .purge_by_key with record_key in purge" do + allow(fastly_service).to receive(:purge_by_key) + purgeable_model.purge + expect(fastly_service).to have_received(:purge_by_key).with(purgeable_model.record_key) + end + + it "calls .purge_by_key with record_key and true in soft_purge" do + allow(fastly_service).to receive(:purge_by_key) + purgeable_model.soft_purge + expect(fastly_service).to have_received(:purge_by_key).with(purgeable_model.record_key, true) + end + + it "calls purge_all class method" do + allow(model_class).to receive(:purge_all) + purgeable_model.purge_all + expect(model_class).to have_received(:purge_all) + end + + it "calls soft_purge_all class method" do + allow(model_class).to receive(:soft_purge_all) + purgeable_model.soft_purge_all + expect(model_class).to have_received(:soft_purge_all) + end + + it "calls the fastly class method" do + allow(model_class).to receive(:fastly) + purgeable_model.fastly + expect(model_class).to have_received(:fastly) + end + + it "calls the service class method" do + allow(model_class).to receive(:service) + purgeable_model.service + expect(model_class).to have_received(:service) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index bee27d325..ec48326a6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -113,7 +113,7 @@ RSpec.configure do |config| stub_request(:any, /res.cloudinary.com/).to_rack("dsdsdsds") stub_request(:post, /api.fastly.com/). - to_return(status: 200, body: "", headers: {}) + to_return(status: 200, body: "".to_json, headers: {}) stub_request(:post, /api.bufferapp.com/). to_return(status: 200, body: { fake_text: "so fake" }.to_json, headers: {})