diff --git a/back/Gemfile b/back/Gemfile index 0293df1d6ccb..ab45046154a9 100644 --- a/back/Gemfile +++ b/back/Gemfile @@ -199,6 +199,7 @@ commercial_engines = [ 'multi_tenancy', 'admin_api', + 'aggressive_caching', 'analysis', 'analytics', 'bulk_import_ideas', diff --git a/back/Gemfile.lock b/back/Gemfile.lock index f17505f6845c..54c6ed258cb9 100644 --- a/back/Gemfile.lock +++ b/back/Gemfile.lock @@ -9,6 +9,13 @@ PATH rails (~> 7.0) ros-apartment (>= 2.9.0) +PATH + remote: engines/commercial/aggressive_caching + specs: + aggressive_caching (0.1.0) + actionpack-action_caching (~> 1.2) + rails (~> 7.0) + PATH remote: engines/commercial/analysis specs: @@ -402,6 +409,8 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionpack-action_caching (1.2.2) + actionpack (>= 4.0.0) actiontext (7.0.8.5) actionpack (= 7.0.8.5) activerecord (= 7.0.8.5) @@ -1245,6 +1254,7 @@ DEPENDENCIES activerecord-postgis-adapter (~> 8.0) acts_as_list (~> 1.1) admin_api! + aggressive_caching! analysis! analytics! annotate diff --git a/back/app/controllers/application_controller.rb b/back/app/controllers/application_controller.rb index 3994e257121e..88d586da6571 100644 --- a/back/app/controllers/application_controller.rb +++ b/back/app/controllers/application_controller.rb @@ -159,3 +159,5 @@ def remove_image_if_requested!(resource, resource_params, image_field_name) resource.public_send(:"remove_#{image_field_name}!") end end + +ApplicationController.include(AggressiveCaching::Patches::ApplicationController) diff --git a/back/app/controllers/web_api/v1/admin_publications_controller.rb b/back/app/controllers/web_api/v1/admin_publications_controller.rb index 868a044c2395..f30ac5f471b7 100644 --- a/back/app/controllers/web_api/v1/admin_publications_controller.rb +++ b/back/app/controllers/web_api/v1/admin_publications_controller.rb @@ -96,3 +96,5 @@ def set_admin_publication authorize @admin_publication end end + +WebApi::V1::AdminPublicationsController.include(AggressiveCaching::Patches::WebApi::V1::AdminPublicationsController) diff --git a/back/app/controllers/web_api/v1/app_configurations_controller.rb b/back/app/controllers/web_api/v1/app_configurations_controller.rb index 0b097ff72b83..2de1df37be45 100644 --- a/back/app/controllers/web_api/v1/app_configurations_controller.rb +++ b/back/app/controllers/web_api/v1/app_configurations_controller.rb @@ -43,3 +43,5 @@ def config_params .permit(:logo, :favicon, settings: {}, style: {}) end end + +WebApi::V1::AppConfigurationsController.include(AggressiveCaching::Patches::WebApi::V1::AppConfigurationsController) diff --git a/back/app/controllers/web_api/v1/areas_controller.rb b/back/app/controllers/web_api/v1/areas_controller.rb index 3ec41e8f5f48..7bb336166f76 100644 --- a/back/app/controllers/web_api/v1/areas_controller.rb +++ b/back/app/controllers/web_api/v1/areas_controller.rb @@ -106,3 +106,5 @@ def set_side_effects_service @side_fx_service = SideFxAreaService.new end end + +WebApi::V1::AreasController.include(AggressiveCaching::Patches::WebApi::V1::AreasController) diff --git a/back/app/controllers/web_api/v1/comments_controller.rb b/back/app/controllers/web_api/v1/comments_controller.rb index 56ac365eee03..33a633f78fca 100644 --- a/back/app/controllers/web_api/v1/comments_controller.rb +++ b/back/app/controllers/web_api/v1/comments_controller.rb @@ -279,3 +279,5 @@ def anonymous_not_allowed? end end end + +WebApi::V1::CommentsController.include(AggressiveCaching::Patches::WebApi::V1::CommentsController) diff --git a/back/app/controllers/web_api/v1/events_controller.rb b/back/app/controllers/web_api/v1/events_controller.rb index 77682ca952dc..2d34e8dd5f19 100644 --- a/back/app/controllers/web_api/v1/events_controller.rb +++ b/back/app/controllers/web_api/v1/events_controller.rb @@ -192,3 +192,5 @@ def sidefx @sidefx ||= SideFxEventService.new end end + +WebApi::V1::EventsController.include(AggressiveCaching::Patches::WebApi::V1::EventsController) diff --git a/back/app/controllers/web_api/v1/folders_controller.rb b/back/app/controllers/web_api/v1/folders_controller.rb index bb51bf49b2e8..71e66e6ca19b 100644 --- a/back/app/controllers/web_api/v1/folders_controller.rb +++ b/back/app/controllers/web_api/v1/folders_controller.rb @@ -126,3 +126,5 @@ def project_folder_params ) end end + +WebApi::V1::FoldersController.include(AggressiveCaching::Patches::WebApi::V1::FoldersController) diff --git a/back/app/controllers/web_api/v1/idea_statuses_controller.rb b/back/app/controllers/web_api/v1/idea_statuses_controller.rb index 17d85d9f6f89..e3b5efc19465 100644 --- a/back/app/controllers/web_api/v1/idea_statuses_controller.rb +++ b/back/app/controllers/web_api/v1/idea_statuses_controller.rb @@ -114,3 +114,5 @@ def max_ordering IdeaStatus.where(code: IdeaStatus::LOCKED_CODES, participation_method: @idea_status.participation_method).maximum(:ordering) || -1 end end + +WebApi::V1::IdeaStatusesController.include(AggressiveCaching::Patches::WebApi::V1::IdeaStatusesController) diff --git a/back/app/controllers/web_api/v1/ideas_controller.rb b/back/app/controllers/web_api/v1/ideas_controller.rb index a4e4529c3e7f..9fe088e9edde 100644 --- a/back/app/controllers/web_api/v1/ideas_controller.rb +++ b/back/app/controllers/web_api/v1/ideas_controller.rb @@ -477,3 +477,4 @@ def not_allowed_update_errors(input) end WebApi::V1::IdeasController.prepend(IdeaAssignment::Patches::WebApi::V1::IdeasController) +WebApi::V1::IdeasController.include(AggressiveCaching::Patches::WebApi::V1::IdeasController) diff --git a/back/app/controllers/web_api/v1/nav_bar_items_controller.rb b/back/app/controllers/web_api/v1/nav_bar_items_controller.rb index edfa9556ceaa..898b0750ae3b 100644 --- a/back/app/controllers/web_api/v1/nav_bar_items_controller.rb +++ b/back/app/controllers/web_api/v1/nav_bar_items_controller.rb @@ -72,3 +72,5 @@ def set_item authorize @item end end + +WebApi::V1::NavBarItemsController.include(AggressiveCaching::Patches::WebApi::V1::NavBarItemsController) diff --git a/back/app/controllers/web_api/v1/official_feedback_controller.rb b/back/app/controllers/web_api/v1/official_feedback_controller.rb index c1f17c0ffb57..6a52cffb7498 100644 --- a/back/app/controllers/web_api/v1/official_feedback_controller.rb +++ b/back/app/controllers/web_api/v1/official_feedback_controller.rb @@ -97,3 +97,5 @@ def official_feedback_params ) end end + +WebApi::V1::OfficialFeedbackController.include(AggressiveCaching::Patches::WebApi::V1::OfficialFeedbackController) diff --git a/back/app/controllers/web_api/v1/phase_custom_fields_controller.rb b/back/app/controllers/web_api/v1/phase_custom_fields_controller.rb index 399673bb77a2..b804767c491e 100644 --- a/back/app/controllers/web_api/v1/phase_custom_fields_controller.rb +++ b/back/app/controllers/web_api/v1/phase_custom_fields_controller.rb @@ -23,3 +23,5 @@ def custom_fields IdeaCustomFieldsService.new(phase.pmethod.custom_form).enabled_fields_with_other_options end end + +WebApi::V1::PhaseCustomFieldsController.include(AggressiveCaching::Patches::WebApi::V1::PhaseCustomFieldsController) diff --git a/back/app/controllers/web_api/v1/phases_controller.rb b/back/app/controllers/web_api/v1/phases_controller.rb index dad8c0461031..c548905e3dea 100644 --- a/back/app/controllers/web_api/v1/phases_controller.rb +++ b/back/app/controllers/web_api/v1/phases_controller.rb @@ -173,3 +173,5 @@ def detect_invalid_timeline_changes end end end + +WebApi::V1::PhasesController.include(AggressiveCaching::Patches::WebApi::V1::PhasesController) diff --git a/back/app/controllers/web_api/v1/project_custom_fields_controller.rb b/back/app/controllers/web_api/v1/project_custom_fields_controller.rb index 89660d9d22fa..4b64b3b1f633 100644 --- a/back/app/controllers/web_api/v1/project_custom_fields_controller.rb +++ b/back/app/controllers/web_api/v1/project_custom_fields_controller.rb @@ -27,3 +27,5 @@ def custom_fields IdeaCustomFieldsService.new(phase.pmethod.custom_form).enabled_fields end end + +WebApi::V1::ProjectCustomFieldsController.include(AggressiveCaching::Patches::WebApi::V1::ProjectCustomFieldsController) diff --git a/back/app/controllers/web_api/v1/projects_controller.rb b/back/app/controllers/web_api/v1/projects_controller.rb index d15481da5019..7dde0fb5118e 100644 --- a/back/app/controllers/web_api/v1/projects_controller.rb +++ b/back/app/controllers/web_api/v1/projects_controller.rb @@ -215,3 +215,5 @@ def check_publication_inconsistencies! end end end + +WebApi::V1::ProjectsController.include(AggressiveCaching::Patches::WebApi::V1::ProjectsController) diff --git a/back/app/controllers/web_api/v1/static_pages_controller.rb b/back/app/controllers/web_api/v1/static_pages_controller.rb index 53f3290cb3cd..4171cdcff8da 100644 --- a/back/app/controllers/web_api/v1/static_pages_controller.rb +++ b/back/app/controllers/web_api/v1/static_pages_controller.rb @@ -81,3 +81,5 @@ def set_page authorize @page end end + +WebApi::V1::StaticPagesController.include(AggressiveCaching::Patches::WebApi::V1::StaticPagesController) diff --git a/back/app/controllers/web_api/v1/topics_controller.rb b/back/app/controllers/web_api/v1/topics_controller.rb index a697158dcde8..d882e3c0fa1d 100644 --- a/back/app/controllers/web_api/v1/topics_controller.rb +++ b/back/app/controllers/web_api/v1/topics_controller.rb @@ -112,3 +112,5 @@ def set_topic authorize @topic end end + +WebApi::V1::TopicsController.include(AggressiveCaching::Patches::WebApi::V1::TopicsController) diff --git a/back/config/environments/test.rb b/back/config/environments/test.rb index 5993ccf69db5..4dd8787f84ef 100644 --- a/back/config/environments/test.rb +++ b/back/config/environments/test.rb @@ -34,7 +34,7 @@ config.action_controller.perform_caching = false # Caching must be turned on for Rack::Attack to work and Rack::Attack tests to pass. - # config.cache_store = :null_store + config.cache_store = :memory_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false diff --git a/back/engines/commercial/aggressive_caching/aggressive_caching.gemspec b/back/engines/commercial/aggressive_caching/aggressive_caching.gemspec new file mode 100644 index 000000000000..6fc505511b91 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/aggressive_caching.gemspec @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +$LOAD_PATH.push File.expand_path('lib', __dir__) + +# Maintain your gem's version: +require 'aggressive_caching/version' + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = 'aggressive_caching' + s.version = AggressiveCaching::VERSION + s.summary = 'Optionally enable aggressive caching for high traffic situations' + s.authors = ['CitizenLab'] + s.licenses = [Gem::Licenses::NONSTANDARD] # ['CitizenLab Commercial License V2'] + s.files = Dir['{app,config,db,lib}/**/*', 'Rakefile', 'README.md'] + + s.add_dependency 'rails', '~> 7.0' + s.add_dependency 'actionpack-action_caching', '~> 1.2' + + s.add_development_dependency 'rspec_api_documentation' + s.add_development_dependency 'rspec-rails' +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/application_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/application_controller.rb new file mode 100644 index 000000000000..66a9fc711ab5 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/application_controller.rb @@ -0,0 +1,45 @@ +module AggressiveCaching + module Patches + module ApplicationController + extend ActiveSupport::Concern + + included do + # Needed to make actionpack-action_caching work with ActionController::API + include ActionController::Caching + # For some Reason, ActionController::Caching is not picking up the Rails + # cache_store by itself. This initialization works, but could probaby be + # improved + self.cache_store = Rails.cache + + skip_after_action :verify_policy_scoped, if: :aggressive_caching_active? + skip_after_action :verify_authorized, if: :aggressive_caching_active? + end + + # Needed to make actionpack-action_caching work with ActionController::API. Fake implemenetation of what is normally provided by ActionController::Base + def action_has_layout=(value) + value + end + + def aggressive_caching_active? + AppConfiguration.instance.feature_activated?('aggressive_caching') + end + + # Helpers for the subclasses to determinte for whom to cache + def caching_and_visitor? + aggressive_caching_active? && current_user.nil? + end + + def caching_and_non_admin? + aggressive_caching_active? && (current_user.nil? || current_user.normal_user?) + end + + # Quite some API responses embed data about whether the current user + # follows the returned resource. This lets us still cache those responses + # for users that are not following anything + def caching_and_not_following? + aggressive_caching_active? && + (current_user.nil? || (current_user.normal_user? && current_user&.follows&.none?)) + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/admin_publications_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/admin_publications_controller.rb new file mode 100644 index 000000000000..9ad24b62647b --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/admin_publications_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module AdminPublicationsController + def self.included(base) + base.class_eval do + # We can only cache for visitors, because permissions play a role in the admin publications shown + with_options if: :caching_and_visitor? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, :status_counts, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/app_configurations_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/app_configurations_controller.rb new file mode 100644 index 000000000000..b27bbd04d202 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/app_configurations_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module AppConfigurationsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_non_admin? do + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/areas_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/areas_controller.rb new file mode 100644 index 000000000000..0cc5d60fd1b2 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/areas_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module AreasController + def self.included(base) + base.class_eval do + with_options if: :caching_and_not_following? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/comments_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/comments_controller.rb new file mode 100644 index 000000000000..b19992144abb --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/comments_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module CommentsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :index, :children, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/content_builder_layouts_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/content_builder_layouts_controller.rb new file mode 100644 index 000000000000..d34e2faafd43 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/content_builder_layouts_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module ContentBuilderLayoutsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_non_admin? do + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/events_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/events_controller.rb new file mode 100644 index 000000000000..5176cd9a0143 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/events_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module EventsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/folders_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/folders_controller.rb new file mode 100644 index 000000000000..d7e84df001ab --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/folders_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module FoldersController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, :by_slug, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/idea_statuses_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/idea_statuses_controller.rb new file mode 100644 index 000000000000..79f97abd9c08 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/idea_statuses_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module IdeaStatusesController + def self.included(base) + base.class_eval do + with_options if: :caching_and_non_admin? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/ideas_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/ideas_controller.rb new file mode 100644 index 000000000000..956535682295 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/ideas_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module IdeasController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + # We need an extra :skip_after_action here, because the + # IdeasController re-specifies the after_action hook explicitly + # and takes precedence + skip_after_action :verify_policy_scoped + + caches_action :index, :index_mini, :filter_counts, :as_markers, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, :by_slug, expires_in: 1.minute + end + caches_action :json_forms_schema, expires_in: 1.day, if: :caching_and_non_admin? + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/nav_bar_items_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/nav_bar_items_controller.rb new file mode 100644 index 000000000000..d042c3b65d35 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/nav_bar_items_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module NavBarItemsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_non_admin? do + skip_after_action :verify_policy_scoped + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/official_feedback_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/official_feedback_controller.rb new file mode 100644 index 000000000000..c4a4809d2c22 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/official_feedback_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module OfficialFeedbackController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/phase_custom_fields_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/phase_custom_fields_controller.rb new file mode 100644 index 000000000000..a7c3d10c29f8 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/phase_custom_fields_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module PhaseCustomFieldsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :json_forms_schema, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/phases_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/phases_controller.rb new file mode 100644 index 000000000000..228281849ff0 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/phases_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module PhasesController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, :submission_count, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/project_custom_fields_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/project_custom_fields_controller.rb new file mode 100644 index 000000000000..6a8d546c95ed --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/project_custom_fields_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module ProjectCustomFieldsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :json_forms_schema, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/projects_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/projects_controller.rb new file mode 100644 index 000000000000..ebc34fd9873c --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/projects_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module ProjectsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_visitor? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, :by_slug, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/static_pages_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/static_pages_controller.rb new file mode 100644 index 000000000000..7312902e85f3 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/static_pages_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module StaticPagesController + def self.included(base) + base.class_eval do + with_options if: :caching_and_non_admin? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, :by_slug, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/topics_controller.rb b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/topics_controller.rb new file mode 100644 index 000000000000..abb34b167ec1 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/app/controllers/aggressive_caching/patches/web_api/v1/topics_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AggressiveCaching + module Patches + module WebApi + module V1 + module TopicsController + def self.included(base) + base.class_eval do + with_options if: :caching_and_not_following? do + caches_action :index, expires_in: 1.minute, cache_path: -> { request.query_parameters } + caches_action :show, expires_in: 1.minute + end + end + end + end + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/lib/aggressive_caching.rb b/back/engines/commercial/aggressive_caching/lib/aggressive_caching.rb new file mode 100644 index 000000000000..2ff82f09aedc --- /dev/null +++ b/back/engines/commercial/aggressive_caching/lib/aggressive_caching.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'actionpack/action_caching' +require 'aggressive_caching/engine' + +module AggressiveCaching + # Your code goes here... +end diff --git a/back/engines/commercial/aggressive_caching/lib/aggressive_caching/engine.rb b/back/engines/commercial/aggressive_caching/lib/aggressive_caching/engine.rb new file mode 100644 index 000000000000..f77a92121333 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/lib/aggressive_caching/engine.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module AggressiveCaching + class Engine < ::Rails::Engine + isolate_namespace AggressiveCaching + + config.to_prepare do + require 'aggressive_caching/feature_specification' + AppConfiguration::Settings.add_feature(AggressiveCaching::FeatureSpecification) + end + end +end diff --git a/back/engines/commercial/aggressive_caching/lib/aggressive_caching/feature_specification.rb b/back/engines/commercial/aggressive_caching/lib/aggressive_caching/feature_specification.rb new file mode 100644 index 000000000000..f2db73c32665 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/lib/aggressive_caching/feature_specification.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Engine namespace +module AggressiveCaching + module FeatureSpecification + # Note that we are extending (not including) here! + extend CitizenLab::Mixins::FeatureSpecification + + # will be used as the property key in the main settings json schema + def self.feature_name + 'aggressive_caching' + end + + def self.feature_title + 'Aggressive caching' + end + + def self.feature_description + <<~DESC + This feature is used to aggressivel cache API responses. + It should always be turned off, unless in exceptional circumstances where we are experiencing major traffic peaks which the platform can no longer handle. + When enabled, some data on the platform might take some time to update after changes have been made.' + DESC + end + + def self.allowed_by_default + true + end + + def self.enabled_by_default + false + end + end +end diff --git a/back/engines/commercial/aggressive_caching/lib/aggressive_caching/version.rb b/back/engines/commercial/aggressive_caching/lib/aggressive_caching/version.rb new file mode 100644 index 000000000000..928fc1d4e0f8 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/lib/aggressive_caching/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module AggressiveCaching + VERSION = '0.1.0' +end diff --git a/back/engines/commercial/aggressive_caching/spec/acceptance/ideas_spec.rb b/back/engines/commercial/aggressive_caching/spec/acceptance/ideas_spec.rb new file mode 100644 index 000000000000..cf4262cb6985 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/spec/acceptance/ideas_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'rspec_api_documentation/dsl' + +resource 'Ideas', :clear_cache, document: false do + before do + header 'Content-Type', 'application/json' + settings = AppConfiguration.instance.settings + settings['aggressive_caching'] = { 'enabled' => true, 'allowed' => true } + AppConfiguration.instance.update!(settings:) + end + + get 'web_api/v1/ideas' do + example 'caches for a visitor' do + expect(Rails.cache.read('views/example.org/web_api/v1/ideas.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/ideas.json')).to be_present + end + + context 'when logged in' do + before { header_token_for create(:user) } + + example 'does not cache' do + expect(Rails.cache.read('views/example.org/web_api/v1/ideas.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/ideas.json')).to be_nil + end + end + + context 'with aggressive caching disabled' do + before do + settings = AppConfiguration.instance.settings + settings['aggressive_caching'] = { 'enabled' => false, 'allowed' => true } + AppConfiguration.instance.update!(settings:) + end + + example 'does not cache' do + expect(Rails.cache.read('views/example.org/web_api/v1/ideas.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/ideas.json')).to be_nil + end + end + end + + get 'web_api/v1/ideas/:id' do + let(:idea) { create(:idea) } + let(:id) { idea.id } + + example 'caches for a visitor' do + expect(Rails.cache.read("views/example.org/web_api/v1/ideas/#{id}.json")).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read("views/example.org/web_api/v1/ideas/#{id}.json")).to be_present + end + + context 'when logged in' do + before { header_token_for create(:user) } + + example 'does not cache' do + expect(Rails.cache.read("views/example.org/web_api/v1/ideas/#{id}.json")).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read("views/example.org/web_api/v1/ideas/#{id}.json")).to be_nil + end + end + + context 'with aggressive caching disabled' do + before do + settings = AppConfiguration.instance.settings + settings['aggressive_caching'] = { 'enabled' => false, 'allowed' => true } + AppConfiguration.instance.update!(settings:) + end + + example 'does not cache' do + expect(Rails.cache.read("views/example.org/web_api/v1/ideas/#{id}.json")).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read("views/example.org/web_api/v1/ideas/#{id}.json")).to be_nil + end + end + end +end diff --git a/back/engines/commercial/aggressive_caching/spec/acceptance/static_pages_spec.rb b/back/engines/commercial/aggressive_caching/spec/acceptance/static_pages_spec.rb new file mode 100644 index 000000000000..72e341628155 --- /dev/null +++ b/back/engines/commercial/aggressive_caching/spec/acceptance/static_pages_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'rspec_api_documentation/dsl' + +resource 'StaticPages', :clear_cache, document: false do + before do + header 'Content-Type', 'application/json' + settings = AppConfiguration.instance.settings + settings['aggressive_caching'] = { 'enabled' => true, 'allowed' => true } + AppConfiguration.instance.update!(settings:) + end + + get 'web_api/v1/static_pages' do + example 'Caches for a visitor' do + expect(Rails.cache.read('views/example.org/web_api/v1/static_pages.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/static_pages.json')).to be_present + end + end +end diff --git a/back/engines/commercial/aggressive_caching/spec/acceptance/topics_spec.rb b/back/engines/commercial/aggressive_caching/spec/acceptance/topics_spec.rb new file mode 100644 index 000000000000..2529a54e4d9d --- /dev/null +++ b/back/engines/commercial/aggressive_caching/spec/acceptance/topics_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'rspec_api_documentation/dsl' + +resource 'Topics', :clear_cache, document: false do + before do + header 'Content-Type', 'application/json' + settings = AppConfiguration.instance.settings + settings['aggressive_caching'] = { 'enabled' => true, 'allowed' => true } + AppConfiguration.instance.update!(settings:) + end + + get 'web_api/v1/topics' do + example 'caches for a visitor' do + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_present + end + + context 'when logged in and following something' do + before { header_token_for create(:follower).user } + + example 'it does not cache' do + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_nil + end + end + + context 'when logged in and not following anything' do + before { header_token_for create(:user) } + + example 'it caches' do + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_present + end + end + + context 'with aggressive caching disabled' do + before do + settings = AppConfiguration.instance.settings + settings['aggressive_caching'] = { 'enabled' => false, 'allowed' => true } + AppConfiguration.instance.update!(settings:) + end + + example 'does not cache' do + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read('views/example.org/web_api/v1/topics.json')).to be_nil + end + end + end + + get 'web_api/v1/topics/:id' do + let(:topic) { create(:topic) } + let(:id) { topic.id } + + example 'caches for a visitor' do + expect(Rails.cache.read("views/example.org/web_api/v1/topics/#{id}.json")).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read("views/example.org/web_api/v1/topics/#{id}.json")).to be_present + end + + context 'when logged in and not following' do + before { header_token_for create(:user) } + + example 'it caches' do + expect(Rails.cache.read("views/example.org/web_api/v1/topics/#{id}.json")).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read("views/example.org/web_api/v1/topics/#{id}.json")).to be_present + end + end + + context 'with aggressive caching disabled' do + before do + settings = AppConfiguration.instance.settings + settings['aggressive_caching'] = { 'enabled' => false, 'allowed' => true } + AppConfiguration.instance.update!(settings:) + end + + example 'does not cache' do + expect(Rails.cache.read("views/example.org/web_api/v1/topics/#{id}.json")).to be_nil + do_request + expect(status).to eq 200 + expect(Rails.cache.read("views/example.org/web_api/v1/topics/#{id}.json")).to be_nil + end + end + end +end diff --git a/back/engines/commercial/content_builder/app/controllers/content_builder/web_api/v1/content_builder_layouts_controller.rb b/back/engines/commercial/content_builder/app/controllers/content_builder/web_api/v1/content_builder_layouts_controller.rb index 170e38f1e7c7..7a5ebc1d4456 100644 --- a/back/engines/commercial/content_builder/app/controllers/content_builder/web_api/v1/content_builder_layouts_controller.rb +++ b/back/engines/commercial/content_builder/app/controllers/content_builder/web_api/v1/content_builder_layouts_controller.rb @@ -106,3 +106,5 @@ def to_boolean(value) end end end + +ContentBuilder::WebApi::V1::ContentBuilderLayoutsController.include(AggressiveCaching::Patches::WebApi::V1::ContentBuilderLayoutsController) diff --git a/back/spec/spec_helper.rb b/back/spec/spec_helper.rb index 2c0a893c519b..3eb5423cf19d 100644 --- a/back/spec/spec_helper.rb +++ b/back/spec/spec_helper.rb @@ -222,6 +222,14 @@ # By default, skip the slow tests and template tests. Can be overriden on the command line. config.filter_run_excluding template_test: true + + config.before(:example, clear_cache: true) do + Rails.cache.clear + end + + config.after(:example, clear_cache: true) do + Rails.cache.clear + end end RSpec::Matchers.define_negated_matcher :not_change, :change