Cache oneboxes in Redis now instead of postgres.

This commit is contained in:
Robin Ward 2013-03-21 13:11:54 -04:00
parent 9d4ecd7ef8
commit babcfe6234
10 changed files with 88 additions and 153 deletions

View file

@ -3,9 +3,7 @@ require_dependency 'oneboxer'
class OneboxController < ApplicationController class OneboxController < ApplicationController
def show def show
Oneboxer.invalidate(params[:url]) if params[:refresh].present? result = Oneboxer.onebox(params[:url], invalidate_oneboxes: params[:refresh] == 'true')
result = Oneboxer.preview(params[:url])
result.strip! if result.present? result.strip! if result.present?
# If there is no result, return a 404 # If there is no result, return a 404

View file

@ -1,8 +0,0 @@
class OneboxRender < ActiveRecord::Base
validates_presence_of :url
validates_presence_of :cooked
validates_presence_of :expires_at
has_many :post_onebox_renders, dependent: :delete_all
has_many :posts, through: :post_onebox_renders
end

View file

@ -219,7 +219,7 @@ class Post < ActiveRecord::Base
doc = Oneboxer.each_onebox_link(cooked) do |url, elem| doc = Oneboxer.each_onebox_link(cooked) do |url, elem|
cached = Oneboxer.render_from_cache(url) cached = Oneboxer.render_from_cache(url)
if cached.present? if cached.present?
elem.swap(cached.cooked) elem.swap(cached)
dirty = true dirty = true
end end
end end

View file

@ -1,6 +0,0 @@
class PostOneboxRender < ActiveRecord::Base
belongs_to :post
belongs_to :onebox_render
validates_uniqueness_of :post_id, scope: :onebox_render_id
end

View file

@ -0,0 +1,9 @@
class RemoveOneboxesFromDb < ActiveRecord::Migration
def up
drop_table :post_onebox_renders
drop_table :onebox_renders
end
def down
end
end

View file

@ -1,4 +1,5 @@
require 'open-uri' require 'open-uri'
require 'digest/sha1'
require_dependency 'oneboxer/base' require_dependency 'oneboxer/base'
require_dependency 'oneboxer/whitelist' require_dependency 'oneboxer/whitelist'
@ -14,7 +15,7 @@ module Oneboxer
end end
def self.default_expiry def self.default_expiry
1.month 1.day
end end
# Return a oneboxer for a given URL # Return a oneboxer for a given URL
@ -79,72 +80,39 @@ module Oneboxer
doc doc
end end
def self.create_post_reference(result, args={}) def self.cache_key_for(url)
result.post_onebox_renders.create(post_id: args[:post_id]) if args[:post_id].present? "onebox:#{Digest::SHA1.hexdigest(url)}"
rescue ActiveRecord::RecordNotUnique
end end
def self.render_from_cache(url, args={}) def self.render_from_cache(url)
result = OneboxRender.where(url: url).first Rails.cache.read(cache_key_for(url))
# Return the result but also create a reference to it
if result.present?
create_post_reference(result, args)
return result
end
nil
end end
# Cache results from a onebox call # Cache results from a onebox call
def self.fetch_and_cache(url, args) def self.fetch_and_cache(url, args)
cooked, preview = onebox_nocache(url) contents = onebox_nocache(url)
return nil if cooked.blank? return nil if contents.blank?
# Store a cooked version in the database Rails.cache.write(cache_key_for(url), contents, expires_in: default_expiry)
OneboxRender.transaction do contents
begin
render = OneboxRender.create(url: url, preview: preview, cooked: cooked, expires_at: Oneboxer.default_expiry.from_now)
create_post_reference(render, args)
rescue ActiveRecord::RecordNotUnique
end
end
[cooked, preview]
end
# Retrieve a preview of a onebox, caching the result for performance
def self.preview(url, args={})
cached = render_from_cache(url, args) unless args[:no_cache].present?
# If we have a preview stored, return that. Otherwise return cooked content.
if cached.present?
return cached.preview if cached.preview.present?
return cached.cooked
end
cooked, preview = fetch_and_cache(url, args)
return preview if preview.present?
cooked
end end
def self.invalidate(url) def self.invalidate(url)
OneboxRender.destroy_all(url: url) Rails.cache.delete(cache_key_for(url))
end end
# Return the cooked content for a url, caching the result for performance # Return the cooked content for a url, caching the result for performance
def self.onebox(url, args={}) def self.onebox(url, args={})
if args[:invalidate_oneboxes].present? if args[:invalidate_oneboxes]
# Remove the onebox from the cache # Remove the onebox from the cache
Oneboxer.invalidate(url) Oneboxer.invalidate(url)
else else
cached = render_from_cache(url, args) unless args[:no_cache].present? contents = render_from_cache(url)
return cached.cooked if cached.present? return contents if contents.present?
end end
fetch_and_cache(url, args)
cooked, preview = fetch_and_cache(url, args)
cooked
end end
end end

View file

@ -46,16 +46,68 @@ describe Oneboxer do
end end
end end
let(:dummy_onebox_url) { "http://dummy.localhost/dummy-object" }
before do before do
Oneboxer.add_onebox DummyOnebox Oneboxer.add_onebox DummyOnebox
@dummy_onebox_url = "http://dummy.localhost/dummy-object"
end end
it 'should have matchers set up by default' do it 'should have matchers set up by default' do
Oneboxer.matchers.should be_present Oneboxer.matchers.should be_present
end end
context 'caching' do
let(:result) { "onebox result string" }
context "with invalidate_oneboxes true" do
it "invalidates the url" do
Oneboxer.expects(:invalidate).with(dummy_onebox_url)
Oneboxer.onebox(dummy_onebox_url, invalidate_oneboxes: true)
end
it "doesn't render from cache" do
Oneboxer.expects(:render_from_cache).never
Oneboxer.onebox(dummy_onebox_url, invalidate_oneboxes: true)
end
it "calls fetch and cache" do
Oneboxer.expects(:fetch_and_cache).returns(result)
Oneboxer.onebox(dummy_onebox_url, invalidate_oneboxes: true).should == result
end
end
context 'with invalidate_oneboxes false' do
it "doesn't invalidate the url" do
Oneboxer.expects(:invalidate).with(dummy_onebox_url).never
Oneboxer.onebox(dummy_onebox_url, invalidate_oneboxes: false)
end
it "returns render_from_cache if present" do
Oneboxer.expects(:render_from_cache).with(dummy_onebox_url).returns(result)
Oneboxer.onebox(dummy_onebox_url, invalidate_oneboxes: false).should == result
end
it "doesn't call fetch_and_cache" do
Oneboxer.expects(:render_from_cache).with(dummy_onebox_url).returns(result)
Oneboxer.expects(:fetch_and_cache).never
Oneboxer.onebox(dummy_onebox_url, invalidate_oneboxes: false)
end
it "calls fetch_and_cache if render from cache is blank" do
Oneboxer.stubs(:render_from_cache)
Oneboxer.expects(:fetch_and_cache).returns(result)
Oneboxer.onebox(dummy_onebox_url, invalidate_oneboxes: false).should == result
end
end
end
context 'find onebox for url' do context 'find onebox for url' do
it 'returns blank with an unknown url' do it 'returns blank with an unknown url' do
@ -63,11 +115,11 @@ describe Oneboxer do
end end
it 'returns something when matched' do it 'returns something when matched' do
Oneboxer.onebox_for_url(@dummy_onebox_url).should be_present Oneboxer.onebox_for_url(dummy_onebox_url).should be_present
end end
it 'returns an instance of our class when matched' do it 'returns an instance of our class when matched' do
Oneboxer.onebox_for_url(@dummy_onebox_url).kind_of?(DummyOnebox).should be_true Oneboxer.onebox_for_url(dummy_onebox_url).kind_of?(DummyOnebox).should be_true
end end
end end
@ -92,69 +144,10 @@ describe Oneboxer do
context 'without caching' do context 'without caching' do
it 'calls the onebox method of our matched class' do it 'calls the onebox method of our matched class' do
Oneboxer.onebox_nocache(@dummy_onebox_url).should == 'dummy!' Oneboxer.onebox_nocache(dummy_onebox_url).should == 'dummy!'
end end
end end
context 'with caching' do
context 'initial cache is empty' do
it 'has no OneboxRender records' do
OneboxRender.count.should == 0
end
it 'calls the onebox_nocache method if there is no cache record yet' do
Oneboxer.expects(:onebox_nocache).with(@dummy_onebox_url).once
Oneboxer.onebox(@dummy_onebox_url)
end
end
context 'caching result' do
before do
@post = Fabricate(:post)
@result = Oneboxer.onebox(@dummy_onebox_url, post_id: @post.id)
@onebox_render = OneboxRender.where(url: @dummy_onebox_url).first
end
it "returns the correct result" do
@result.should == 'dummy!'
end
it "created a OneboxRender record with the url" do
@onebox_render.should be_present
end
it "created a OneboxRender record with the url" do
@onebox_render.url.should == @dummy_onebox_url
end
it "associated the render with a post" do
@onebox_render.posts.should == [@post]
end
it "has an expires_at value" do
@onebox_render.expires_at.should be_present
end
it "doesn't call onebox_nocache on a cache hit" do
Oneboxer.expects(:onebox_nocache).never
Oneboxer.onebox(@dummy_onebox_url).should == 'dummy!'
end
context 'invalidating cache' do
it "deletes the onebox render" do
Oneboxer.expects(:onebox_nocache).once.returns('new cache value!')
Oneboxer.onebox(@dummy_onebox_url, invalidate_oneboxes: true).should == 'new cache value!'
end
end
end
end
context 'each_onebox_link' do context 'each_onebox_link' do
before do before do

View file

@ -5,8 +5,8 @@ describe OneboxController do
let(:url) { "http://google.com" } let(:url) { "http://google.com" }
it 'invalidates the cache if refresh is passed' do it 'invalidates the cache if refresh is passed' do
Oneboxer.expects(:invalidate).with(url) Oneboxer.expects(:onebox).with(url, invalidate_oneboxes: true)
xhr :get, :show, url: url, refresh: true xhr :get, :show, url: url, refresh: 'true'
end end
describe "found onebox" do describe "found onebox" do
@ -14,7 +14,7 @@ describe OneboxController do
let(:body) { "this is the onebox body"} let(:body) { "this is the onebox body"}
before do before do
Oneboxer.expects(:preview).with(url).returns(body) Oneboxer.expects(:onebox).with(url, invalidate_oneboxes: false).returns(body)
xhr :get, :show, url: url xhr :get, :show, url: url
end end
@ -31,13 +31,13 @@ describe OneboxController do
describe "missing onebox" do describe "missing onebox" do
it "returns 404 if the onebox is nil" do it "returns 404 if the onebox is nil" do
Oneboxer.expects(:preview).with(url).returns(nil) Oneboxer.expects(:onebox).with(url, invalidate_oneboxes: false).returns(nil)
xhr :get, :show, url: url xhr :get, :show, url: url
response.response_code.should == 404 response.response_code.should == 404
end end
it "returns 404 if the onebox is an empty string" do it "returns 404 if the onebox is an empty string" do
Oneboxer.expects(:preview).with(url).returns(" \t ") Oneboxer.expects(:onebox).with(url, invalidate_oneboxes: false).returns(" \t ")
xhr :get, :show, url: url xhr :get, :show, url: url
response.response_code.should == 404 response.response_code.should == 404
end end

View file

@ -1,11 +0,0 @@
require 'spec_helper'
describe OneboxRender do
it { should validate_presence_of :url }
it { should validate_presence_of :cooked }
it { should validate_presence_of :expires_at }
it { should have_many :post_onebox_renders }
it { should have_many :posts }
end

View file

@ -1,8 +0,0 @@
require 'spec_helper'
describe PostOneboxRender do
it { should belong_to :onebox_render }
it { should belong_to :post }
end