From e035109c6017e29161bb92fc829d1d25c31f48bb Mon Sep 17 00:00:00 2001 From: Anton K Date: Fri, 3 May 2019 18:34:01 +0300 Subject: [PATCH] Enable brotli compression in the caches_page --- .travis.yml | 4 +- CHANGELOG.md | 6 ++ README.md | 31 +++++++++++ actionpack-page_caching.gemspec | 3 + lib/action_controller/caching/pages.rb | 77 +++++++++++++++++++------- lib/action_controller/page_caching.rb | 2 + lib/actionpack/page_caching.rb | 2 + lib/actionpack/page_caching/railtie.rb | 2 + test/caching_test.rb | 50 +++++++++++++---- 9 files changed, 146 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3667ef6..54e3c31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ cache: bundler: true before_install: - - gem install bundler + - "travis_retry gem update --system 2.7.9" + - "travis_retry gem install bundler -v '1.17.3'" + - "travis_retry gem install brotli" rvm: - 2.4.6 diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f7cca..71c7682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.2.4 Unreleased + +* Allow to use brotli compression + + *Anton Kolodii* + ## 1.1.1 (September 25, 2018) * Fixes handling of several forward slashes as root path. diff --git a/README.md b/README.md index 2b7c75e..462b4e4 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,37 @@ Additionally, you can expire caches using that act on changes in the model to determine when a cache is supposed to be expired. +#### Compression Algorithm + +To use brotli manually install gem 'brotli' inside Gemfile +``` ruby + gem 'brotli' +``` + +To use both gzip and brotli, or just skip compressions, because this is default value + +``` ruby +class WeblogController < ActionController::Base + caches_page :show, :new, compressions: {gzip: Zlib::BEST_COMPRESSION, brotli: 9} +end +``` + +To use only gzip + +``` ruby +class WeblogController < ActionController::Base + caches_page :show, :new, compressions: {gzip: Zlib::BEST_COMPRESSION} +end +``` + +To use only brotli + +``` ruby +class WeblogController < ActionController::Base + caches_page :show, :new, compressions: {brotli: 9} +end +``` + Contributing ------------ diff --git a/actionpack-page_caching.gemspec b/actionpack-page_caching.gemspec index a495a7a..e9d3bc5 100644 --- a/actionpack-page_caching.gemspec +++ b/actionpack-page_caching.gemspec @@ -17,5 +17,8 @@ Gem::Specification.new do |gem| gem.add_dependency "actionpack", ">= 5.0.0" + gem.add_development_dependency 'brotli', '>= 0.2.0' gem.add_development_dependency "mocha" + + gem.post_install_message = "To use brotli compression you have to manually add gem 'brotli' to Gemfile" end diff --git a/lib/action_controller/caching/pages.rb b/lib/action_controller/caching/pages.rb index c364a5b..c95353b 100644 --- a/lib/action_controller/caching/pages.rb +++ b/lib/action_controller/caching/pages.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "fileutils" require "uri" require "active_support/core_ext/class/attribute_accessors" @@ -5,6 +7,8 @@ module ActionController module Caching + COMPRESSIONS_DEFAULTS = {gzip: 9, brotli: 9} # ::Zlib::BEST_COMPRESSION + # Page caching is an approach to caching where the entire action output of is # stored as a HTML file that the web server can serve without going through # Action Pack. This is the fastest way to cache your content as opposed to going @@ -60,6 +64,9 @@ module Pages # or :best_speed or an integer configuring the compression level. class_attribute :page_cache_compression self.page_cache_compression ||= false + + class_attribute :page_cache_compressions + self.page_cache_compressions ||= COMPRESSIONS_DEFAULTS end class PageCache #:nodoc: @@ -75,9 +82,9 @@ def expire(path) end end - def cache(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION) + def cache(content, path, extension = nil, compressions: COMPRESSIONS_DEFAULTS) instrument :write_page, path do - write(content, cache_path(path, extension), gzip) + write(content, cache_path(path, extension), compressions: compressions) end end @@ -168,16 +175,22 @@ def delete(path) File.delete(path) if File.exist?(path) File.delete(path + ".gz") if File.exist?(path + ".gz") + File.delete(path + ".br") if File.exist?(path + ".br") end - def write(content, path, gzip) + def write(content, path, compressions:) return unless path FileUtils.makedirs(File.dirname(path)) File.open(path, "wb+") { |f| f.write(content) } - if gzip - Zlib::GzipWriter.open(path + ".gz", gzip) { |f| f.write(content) } + if compressions[:gzip] + Zlib::GzipWriter.open(path + ".gz", compressions[:gzip]) { |f| f.write(content) } + end + + if compressions[:brotli] + brotli = ::Brotli.deflate(content, mode: :text, quality: compressions[:brotli]) + File.atomic_write(path + ".br") { |f| f.write(brotli) } end end @@ -199,9 +212,9 @@ def expire_page(path) # Manually cache the +content+ in the key determined by +path+. # # cache_page "I'm the cached content", "/lists/show" - def cache_page(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION) + def cache_page(content, path, extension = nil, compressions: COMPRESSIONS_DEFAULTS) if perform_caching - page_cache.cache(content, path, extension, gzip) + page_cache.cache(content, path, extension, compressions: compressions) end end @@ -218,26 +231,50 @@ def cache_page(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION) # caches_page :index, if: Proc.new { !request.format.json? } # # # don't gzip images - # caches_page :image, gzip: false + # caches_page :image, compressions: false def caches_page(*actions) if perform_caching options = actions.extract_options! - gzip_level = options.fetch(:gzip, page_cache_compression) - gzip_level = \ - case gzip_level - when Symbol - Zlib.const_get(gzip_level.upcase) - when Integer - gzip_level + compressions = options.fetch(:compressions, page_cache_compressions) + + compressions = + case compressions when false - nil + {} else - Zlib::BEST_COMPRESSION + compressions end + if options.key?(:gzip) + ActiveSupport::Deprecation.warn( + "actionpack-page-caching now support brotli compression.\n + Using gzip directly is deprecated. instead of\n caches_page :index, gzip: Zlib::BEST_COMPRESSION \n + please use\n caches_page :index, compressions: {gzip: Zlib::BEST_COMPRESSION, brotli: 9}" + ) + + gzip_level = options.fetch(:gzip, page_cache_compression) + gzip_level = \ + case gzip_level + when Symbol + Zlib.const_get(gzip_level.upcase) + when Integer + gzip_level + when false + nil + else + Zlib::BEST_COMPRESSION + end + + compressions[:gzip] = gzip_level + end + + if compressions.key?(:brotli) + require 'brotli' + end + after_action({ only: actions }.merge(options)) do |c| - c.cache_page(nil, nil, gzip_level) + c.cache_page(nil, nil, compressions: compressions) end end end @@ -272,7 +309,7 @@ def expire_page(options = {}) # request being handled is used. # # cache_page "I'm the cached content", controller: "lists", action: "show" - def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION) + def cache_page(content = nil, options = nil, compressions: COMPRESSIONS_DEFAULTS) if perform_caching? && caching_allowed? path = \ case options @@ -294,7 +331,7 @@ def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION) extension = ".#{type_symbol}" end - page_cache.cache(content || response.body, path, extension, gzip) + page_cache.cache(content || response.body, path, extension, compressions: compressions) end end diff --git a/lib/action_controller/page_caching.rb b/lib/action_controller/page_caching.rb index 74a6b96..7855a8a 100644 --- a/lib/action_controller/page_caching.rb +++ b/lib/action_controller/page_caching.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "action_controller/caching/pages" module ActionController diff --git a/lib/actionpack/page_caching.rb b/lib/actionpack/page_caching.rb index 3458374..c94f527 100644 --- a/lib/actionpack/page_caching.rb +++ b/lib/actionpack/page_caching.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + require "actionpack/page_caching/railtie" diff --git a/lib/actionpack/page_caching/railtie.rb b/lib/actionpack/page_caching/railtie.rb index 1c1f383..91c73b6 100644 --- a/lib/actionpack/page_caching/railtie.rb +++ b/lib/actionpack/page_caching/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/railtie" module ActionPack diff --git a/test/caching_test.rb b/test/caching_test.rb index 49f2b5b..f8ffe0b 100644 --- a/test/caching_test.rb +++ b/test/caching_test.rb @@ -119,9 +119,11 @@ class PageCachingTestController < CachingController caches_page :ok, :no_content, if: Proc.new { |c| !c.request.format.json? } caches_page :found, :not_found caches_page :about_me - caches_page :default_gzip - caches_page :no_gzip, gzip: false - caches_page :gzip_level, gzip: :best_speed + caches_page :default_compressions + caches_page :no_gzip, compressions: {gzip: false} + caches_page :gzip_level, compressions: {gzip: 1} + caches_page :no_brotli, compressions: {brotli: false} + caches_page :brotli_level, compressions: {brotli: 1} def ok render html: "ok" @@ -144,8 +146,8 @@ def custom_path cache_page(nil, "/index.html") end - def default_gzip - render html: "default_gzip" + def default_compressions + render html: "default_compressions" end def no_gzip @@ -156,6 +158,14 @@ def gzip_level render html: "gzip_level" end + def no_brotli + render html: "no_brotli" + end + + def brotli_level + render html: "brotli_level" + end + def expire_custom_path expire_page("/index.html") head :ok @@ -261,6 +271,7 @@ def test_should_gzip_cache get :expire_custom_path assert_page_not_cached :index, controller: ".", format: "html.gz" + assert_page_not_cached :index, controller: ".", format: "html.br" end def test_should_allow_to_disable_gzip @@ -273,13 +284,23 @@ def test_should_allow_to_disable_gzip assert_page_not_cached :no_gzip, format: "html.gz" end - def test_should_use_config_gzip_by_default + def test_should_allow_to_disable_brotli + draw do + get "/page_caching_test/no_brotli", to: "page_caching_test#no_brotli" + end + + get :no_brotli + assert_page_cached :no_brotli, format: "html" + assert_page_not_cached :no_brotli, format: "html.br" + end + + def test_should_use_config_compressions_by_default draw do - get "/page_caching_test/default_gzip", to: "page_caching_test#default_gzip" + get "/page_caching_test/default_compressions", to: "page_caching_test#default_compressions" end - @controller.expects(:cache_page).with(nil, nil, Zlib::BEST_COMPRESSION) - get :default_gzip + @controller.expects(:cache_page).with(nil, nil, compressions: {gzip: Zlib::BEST_COMPRESSION, brotli: 9}) + get :default_compressions end def test_should_set_gzip_level @@ -287,10 +308,19 @@ def test_should_set_gzip_level get "/page_caching_test/gzip_level", to: "page_caching_test#gzip_level" end - @controller.expects(:cache_page).with(nil, nil, Zlib::BEST_SPEED) + @controller.expects(:cache_page).with(nil, nil, compressions: {gzip: Zlib::BEST_SPEED}) get :gzip_level end + def test_should_set_brotli_level + draw do + get "/page_caching_test/brotli_level", to: "page_caching_test#brotli_level" + end + + @controller.expects(:cache_page).with(nil, nil, compressions: {brotli: 1}) + get :brotli_level + end + def test_should_cache_without_trailing_slash_on_url @controller.class.cache_page "cached content", "/page_caching_test/trailing_slash" assert_page_cached :trailing_slash, content: "cached content"