remove rack cache, it has been causing trouble

instead implement an aggressive anonymous cache that is stored in redis
this cache is sitting in the front of the middleware stack enabled only in production
TODO: expire it more intelligently when stuff is created
This commit is contained in:
Sam 2013-10-16 16:39:18 +11:00
parent ff966e3276
commit 3d647a4b41
12 changed files with 180 additions and 115 deletions

View File

@ -192,10 +192,6 @@ gem 'flamegraph', git: 'https://github.com/SamSaffron/flamegraph.git', require:
gem 'rack-mini-profiler', git: 'https://github.com/MiniProfiler/rack-mini-profiler.git', require: false gem 'rack-mini-profiler', git: 'https://github.com/MiniProfiler/rack-mini-profiler.git', require: false
# used for caching, optional # used for caching, optional
# redis-rack-cache is missing a sane expiry policy, it hogs redis
# https://github.com/jodosha/redis-store/pull/183
gem 'redis-rack-cache', git: 'https://github.com/SamSaffron/redis-rack-cache.git', require: false
gem 'rack-cache', require: false
gem 'rack-cors', require: false gem 'rack-cors', require: false
gem 'unicorn', require: false gem 'unicorn', require: false
gem 'puma', require: false gem 'puma', require: false

View File

@ -54,14 +54,6 @@ GIT
redis redis
thin thin
GIT
remote: https://github.com/SamSaffron/redis-rack-cache.git
revision: 379ef30e31d4e185cb1d7f8badca0cc06403eba2
specs:
redis-rack-cache (1.2.1)
rack-cache (~> 1.2)
redis-store (~> 1.1.0)
GIT GIT
remote: https://github.com/SamSaffron/sprockets.git remote: https://github.com/SamSaffron/sprockets.git
revision: bacf2ec4d4d10cd8d1ab25a6360740314c512237 revision: bacf2ec4d4d10cd8d1ab25a6360740314c512237
@ -526,7 +518,6 @@ DEPENDENCIES
pry-rails pry-rails
puma puma
qunit-rails qunit-rails
rack-cache
rack-cors rack-cors
rack-mini-profiler! rack-mini-profiler!
rails (= 3.2.12) rails (= 3.2.12)
@ -536,7 +527,6 @@ DEPENDENCIES
rb-inotify (~> 0.9) rb-inotify (~> 0.9)
redcarpet redcarpet
redis redis
redis-rack-cache!
redis-rails redis-rails
rest-client rest-client
rinku rinku

View File

@ -23,7 +23,7 @@ GIT
GIT GIT
remote: git://github.com/rails/rails.git remote: git://github.com/rails/rails.git
revision: cfd9186e34a25a29f7cd2018e91548f5d1be22af revision: 0785008a64f4af8d09379683303d49d160cd444d
branch: 4-0-stable branch: 4-0-stable
specs: specs:
actionmailer (4.0.0) actionmailer (4.0.0)
@ -111,14 +111,6 @@ GIT
redis redis
thin thin
GIT
remote: https://github.com/SamSaffron/redis-rack-cache.git
revision: 379ef30e31d4e185cb1d7f8badca0cc06403eba2
specs:
redis-rack-cache (1.2.1)
rack-cache (~> 1.2)
redis-store (~> 1.1.0)
GIT GIT
remote: https://github.com/callahad/omniauth-browserid.git remote: https://github.com/callahad/omniauth-browserid.git
revision: af62d667626c1622de6fe13b60849c3640765ab1 revision: af62d667626c1622de6fe13b60849c3640765ab1
@ -281,7 +273,7 @@ GEM
mocha (0.14.0) mocha (0.14.0)
metaclass (~> 0.0.1) metaclass (~> 0.0.1)
mock_redis (0.9.0) mock_redis (0.9.0)
multi_json (1.8.1) multi_json (1.8.2)
multipart-post (1.2.0) multipart-post (1.2.0)
mustache (0.99.4) mustache (0.99.4)
net-scp (1.1.2) net-scp (1.1.2)
@ -340,8 +332,6 @@ GEM
qunit-rails (0.0.4) qunit-rails (0.0.4)
railties (>= 3.2.3) railties (>= 3.2.3)
rack (1.5.2) rack (1.5.2)
rack-cache (1.2)
rack (>= 0.4)
rack-cors (0.2.8) rack-cors (0.2.8)
rack rack
rack-openid (1.3.1) rack-openid (1.3.1)
@ -531,7 +521,6 @@ DEPENDENCIES
pry-rails pry-rails
puma puma
qunit-rails qunit-rails
rack-cache
rack-cors rack-cors
rack-mini-profiler! rack-mini-profiler!
rails! rails!
@ -542,7 +531,6 @@ DEPENDENCIES
rb-inotify (~> 0.9) rb-inotify (~> 0.9)
redcarpet redcarpet
redis redis
redis-rack-cache!
redis-rails! redis-rails!
rest-client rest-client
rinku rinku

View File

@ -155,35 +155,15 @@ class ApplicationController < ActionController::Base
end end
def can_cache_content? def can_cache_content?
# Don't cache unless we're in production mode !current_user.present?
return false unless Rails.env.production? || Rails.env == "profile"
# Don't cache logged in users
return false if current_user.present?
true
end end
# Our custom cache method # Our custom cache method
def discourse_expires_in(time_length) def discourse_expires_in(time_length)
return unless can_cache_content? return unless can_cache_content?
expires_in time_length, public: true Middleware::AnonymousCache.anon_cache(request.env, 1.minute)
end end
# Helper method - if no logged in user (anonymous), use Rails' conditional GET
# support. Should be very fast behind a cache.
def anonymous_etag(*args)
if can_cache_content?
yield if stale?(*args)
# Add a one minute expiry
expires_in 1.minute, public: true
else
yield
end
end
def fetch_user_from_params def fetch_user_from_params
username_lower = params[:username].downcase username_lower = params[:username].downcase
username_lower.gsub!(/\.json$/, '') username_lower.gsub!(/\.json$/, '')

View File

@ -22,7 +22,8 @@ class ListController < ApplicationController
[:latest, :hot].each do |filter| [:latest, :hot].each do |filter|
define_method("#{filter}_feed") do define_method("#{filter}_feed") do
anonymous_etag(@category) do discourse_expires_in 1.minute
@title = "#{filter.capitalize} Topics" @title = "#{filter.capitalize} Topics"
@link = "#{Discourse.base_url}/#{filter}" @link = "#{Discourse.base_url}/#{filter}"
@description = I18n.t("rss_description.#{filter}") @description = I18n.t("rss_description.#{filter}")
@ -31,7 +32,6 @@ class ListController < ApplicationController
render 'list', formats: [:rss] render 'list', formats: [:rss]
end end
end end
end
def topics_by def topics_by
list_opts = build_topic_list_options list_opts = build_topic_list_options
@ -99,7 +99,8 @@ class ListController < ApplicationController
guardian.ensure_can_see!(@category) guardian.ensure_can_see!(@category)
anonymous_etag(@category) do discourse_expires_in 1.minute
@title = @category.name @title = @category.name
@link = "#{Discourse.base_url}/category/#{@category.slug}" @link = "#{Discourse.base_url}/category/#{@category.slug}"
@description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}"
@ -107,7 +108,6 @@ class ListController < ApplicationController
@topic_list = TopicQuery.new.list_new_in_category(@category) @topic_list = TopicQuery.new.list_new_in_category(@category)
render 'list', formats: [:rss] render 'list', formats: [:rss]
end end
end
def popular_redirect def popular_redirect
# We've renamed popular to latest. Use a redirect until we're sure we can # We've renamed popular to latest. Use a redirect until we're sure we can

View File

@ -40,7 +40,8 @@ class TopicsController < ApplicationController
return redirect_to(topic.relative_url) return redirect_to(topic.relative_url)
end end
anonymous_etag(@topic_view.topic) do discourse_expires_in 1.minute
redirect_to_correct_topic && return if slugs_do_not_match redirect_to_correct_topic && return if slugs_do_not_match
# render workaround pseudo-static HTML page for old crawlers which ignores <noscript> # render workaround pseudo-static HTML page for old crawlers which ignores <noscript>
@ -54,7 +55,6 @@ class TopicsController < ApplicationController
end end
perform_show_response perform_show_response
end
canonical_url @topic_view.canonical_path canonical_url @topic_view.canonical_path
end end
@ -75,11 +75,11 @@ class TopicsController < ApplicationController
only_moderator_liked: params[:only_moderator_liked].to_s == "true" only_moderator_liked: params[:only_moderator_liked].to_s == "true"
) )
anonymous_etag(@topic_view.topic) do discourse_expires_in 1.minute
wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false) wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false)
render_json_dump(wordpress_serializer) render_json_dump(wordpress_serializer)
end end
end
def posts def posts
@ -272,10 +272,9 @@ class TopicsController < ApplicationController
def feed def feed
@topic_view = TopicView.new(params[:topic_id]) @topic_view = TopicView.new(params[:topic_id])
anonymous_etag(@topic_view.topic) do discourse_expires_in 1.minute
render 'topics/show', formats: [:rss] render 'topics/show', formats: [:rss]
end end
end
private private

View File

@ -44,5 +44,7 @@ Discourse::Application.configure do
require 'middleware/turbo_dev' require 'middleware/turbo_dev'
config.middleware.insert 0, Middleware::TurboDev config.middleware.insert 0, Middleware::TurboDev
config.enable_anon_caching = false
end end

View File

@ -68,9 +68,6 @@ Discourse::Application.configure do
# this will cause all handlebars templates to be pre-compiles, making your page faster # this will cause all handlebars templates to be pre-compiles, making your page faster
config.handlebars.precompile = true config.handlebars.precompile = true
# this setting enables rack_cache so it caches various requests in redis
config.enable_rack_cache = true
# allows admins to use mini profiler # allows admins to use mini profiler
config.enable_mini_profiler = true config.enable_mini_profiler = true

View File

@ -40,9 +40,6 @@ Discourse::Application.configure do
# precompile handlebar assets # precompile handlebar assets
config.handlebars.precompile = true config.handlebars.precompile = true
# this setting enable rack_cache so it caches various requests in redis
config.enable_rack_cache = false
# allows users to use mini profiler # allows users to use mini profiler
config.enable_mini_profiler = false config.enable_mini_profiler = false

View File

@ -1,29 +1,12 @@
if Rails.configuration.respond_to?(:enable_rack_cache) && Rails.configuration.enable_rack_cache require_dependency "middleware/anonymous_cache"
require 'rack-cache'
require 'redis-rack-cache'
# by default we will cache up to 3 minutes in redis, if you want to cut down on redis usage enabled = if Rails.configuration.respond_to?(:enable_anon_caching)
# cut down this number Rails.configuration.enable_anon_caching
RedisRackCache.max_cache_seconds = 60 * 3
url = DiscourseRedis.url
class Rack::Cache::Discourse < Rack::Cache::Context
def initialize(app, options={})
@app = app
super
end
def call(env)
if CurrentUser.has_auth_cookie?(env)
@app.call(env)
else else
super Rails.env.production?
end
end
end end
Rails.configuration.middleware.insert 0, Rack::Cache::Discourse, if enabled
metastore: "#{url}/metastore", Rails.configuration.middleware.insert 0, Middleware::AnonymousCache
entitystore: "#{url}/entitystore",
verbose: !Rails.env.production?
end end

View File

@ -0,0 +1,92 @@
module Middleware
class AnonymousCache
def self.anon_cache(env, duration)
env["ANON_CACHE_DURATION"] = duration
end
class Helper
def initialize(env)
@env = env
end
def cache_key
@cache_key ||= "ANON_CACHE_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}"
end
def cache_key_body
@cache_key_body ||= "#{cache_key}_body"
end
def cache_key_other
@cache_key_other || "#{cache_key}_other"
end
def get?
@env["REQUEST_METHOD"] == "GET"
end
def cacheable?
!!(!CurrentUser.has_auth_cookie?(@env) && get?)
end
def cached
if body = $redis.get(cache_key_body)
if other = $redis.get(cache_key_other)
other = JSON.parse(other)
[other[0], other[1], [body]]
end
end
end
def cache_duration
@env["ANON_CACHE_DURATION"]
end
# NOTE in an ideal world cache still serves out cached content except for one magic worker
# that fills it up, this avoids a herd killing you, we can probably do this using a job or redis tricks
# but coordinating this is tricky
def cache(result)
status,headers,response = result
if status == 200 && cache_duration
headers_stripped = headers.dup.delete_if{|k,v| ["Set-Cookie","X-MiniProfiler-Ids"].include? k}
parts = []
response.each do |part|
parts << part
end
$redis.setex(cache_key_body, cache_duration, parts.join)
$redis.setex(cache_key_other, cache_duration, [status,headers_stripped].to_json)
else
parts = response
end
[status,headers,parts]
end
def clear_cache
$redis.del(cache_key_body)
$redis.del(cache_key_other)
end
end
def initialize(app, settings={})
@app = app
end
def call(env)
helper = Helper.new(env)
if helper.cacheable?
helper.cached or helper.cache(@app.call(env))
else
@app.call(env)
end
end
end
end

View File

@ -0,0 +1,41 @@
require "spec_helper"
require_dependency "middleware/anonymous_cache"
describe Middleware::AnonymousCache::Helper do
def new_helper(env={})
Middleware::AnonymousCache::Helper.new({
"HTTP_HOST" => "http://test.com",
"REQUEST_URI" => "/path?bla=1",
"REQUEST_METHOD" => "GET"
}.merge(env))
end
context "cachable?" do
it "true by default" do
new_helper.cacheable?.should be_true
end
it "is false for non GET" do
new_helper("ANON_CACHE_DURATION" => 10, "REQUEST_METHOD" => "POST").cacheable?.should be_false
end
end
context "cached" do
let!(:helper) do
new_helper("ANON_CACHE_DURATION" => 10)
end
after do
helper.clear_cache
end
it "returns cached data for cached requests" do
helper.cached.should be_nil
helper.cache([200, {"HELLO" => "WORLD"}, ["hello ", "world"]])
helper.cached.should == [200, {"HELLO" => "WORLD"}, ["hello world"]]
end
end
end