Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

in the middle of a rewrite for rails 3, not relying on anything but R…

…ack::Cache (and other middlewares) in future.
  • Loading branch information...
commit 8eb0a2e8f86024f332b552c0c6dee91e68f5b2d1 1 parent 024583b
@svenfuchs authored
View
2  MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2008 Sven Fuchs
+Copyright (c) 2008-2010 Sven Fuchs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
View
9 README.markdown
@@ -1,4 +1,11 @@
-## Page Cache Tagging
+## Cache Reference Tagging
+
+BIG REWRITE FOR RAILS 3 AHEAD.
+
+Won't rely on anything but Rack::Cache in future, i.e. no Rails page caching.
+
+
+THE FOLLOWING ONLY APPLIES TO THE TAG rails-2.x
Largely inspired by Rick Olson's [Referenced Page Caching](http://svn.techno-weenie.net/projects/plugins/referenced_page_caching/)
this plugin uses a more normalized database schema for better performance
View
14 db/migrate/20080401000020_create_cached_page_references.rb
@@ -1,14 +0,0 @@
-class CreateCachedPageReferences < ActiveRecord::Migration
- def self.up
- create_table :cached_page_references, :force => true do |t|
- t.references :cached_page
- t.integer :object_id
- t.string :object_type
- t.string :method
- end
- end
-
- def self.down
- drop_table :cached_page_references
- end
-end
View
15 db/migrate/20080401000021_create_cached_pages.rb
@@ -1,15 +0,0 @@
-class CreateCachedPages < ActiveRecord::Migration
- def self.up
- create_table :cached_pages, :force => true do |t|
- t.references :site
- t.references :section
- t.string :url
- t.datetime :updated_at
- t.datetime :cleared_at
- end
- end
-
- def self.down
- drop_table :cached_pages
- end
-end
View
3  lib/cache_references.rb
@@ -0,0 +1,3 @@
+module CacheReferences
+ autoload :MethodCallTracking, 'cache_references/method_call_tracking'
+end
View
42 lib/cache_references/method_call_tracking.rb
@@ -8,16 +8,20 @@
# the) first call. The given array will then equal [[the_object, :foo]].
module CacheReferences
module MethodCallTracking
- def track_method_calls(tracker, *methods)
+ def track_method_calls(tracker, methods)
meta_class = (class << self; self; end)
methods.each do |method|
- meta_class.send :define_method, method do |*args|
+ meta_class.send(:define_method, method) do |*args|
tracker << [self, method]
- meta_class.send :remove_method, method
+ meta_class.send(:remove_method, method)
super
end
end
end
+
+ def reference_tag
+ "#{self.class.name.underscore}-#{id}"
+ end
# Tracks method access on trackable objects. Trackables can be given as
#
@@ -35,44 +39,44 @@ def track_method_calls(tracker, *methods)
class Tracker
attr_reader :references
- def initialize
+ def initialize(owner = nil, trackables = nil)
@references = []
+ track(owner, trackables) if owner
end
- def track(owner, *trackables)
+ def track(owner, trackables)
trackables.each do |trackable|
- trackable = { trackable => nil } unless trackable.is_a? Hash
+ trackable = { trackable => nil } unless trackable.is_a?(Hash)
trackable.each do |trackable, methods|
trackable = resolve_trackable(owner, trackable)
track_methods(trackable, methods) unless trackable.nil?
end
end
end
+
+ def tags
+ references.map { |reference|reference.first.reference_tag }.uniq.join(',')
+ end
protected
- # Resolves the trackable by looking it up on the owner. Trackables will be
+ # Resolves the trackable by looking it up on the owner. Trackables will be
# interpreted as instance variables when they start with an @ and as method
# names otherwise.
def resolve_trackable(owner, trackable)
- case trackable.to_s
- when /^@/ then owner.instance_variable_get(trackable.to_sym)
- else owner.send(trackable.to_sym)
- end
+ trackable.to_s[0, 1] == '@' ?
+ owner.instance_variable_get(trackable.to_sym) :
+ owner.send(trackable.to_sym)
end
# Wraps the trackable into a MethodReadObserver and registers itself as an observer.
# Sets up tracking for the read_attribute method when the methods argument is nil.
# Sets up tracking for any other given methods otherwise.
def track_methods(trackable, methods)
- methods ||= :read_attribute
- methods = [methods] if methods && !methods.is_a?(Array)
-
- if trackable.is_a? Array
- trackable.each { |trackable| track_methods trackable, methods }
- else
- trackable.track_method_calls(references, *methods) unless methods.empty?
- end
+ methods = Array(methods || :read_attribute)
+ Array(trackable).each do |trackable|
+ trackable.track_method_calls(references, methods)
+ end unless methods.empty?
end
end
end
View
99 lib/cache_references/page_caching.rb
@@ -1,99 +0,0 @@
-require 'cache_references/method_call_tracking'
-
-module CacheReferences
- module PageCaching
- module ActMacro
-
- # Caches the actions using the page-caching approach and sets up reference
- # tracking for given actions and objects
- #
- # caches_page_with_references :index, :show, :track => ['@article', '@articles', {'@site' => :tag_counts}]
- #
- def caches_page_with_references(*actions)
- tracks_cache_references(*actions)
-
- unless caches_page_with_references?
- alias_method_chain :caching_allowed, :skipping
- end
-
- options = actions.extract_options!
- caches_page *actions
- end
-
- # Sets up reference tracking for given actions and objects
- #
- # tracks_cache_references :index, :show, :track => ['@article', '@articles', {'@site' => :tag_counts}]
- #
- def tracks_cache_references(*actions)
- unless tracks_cache_references?
- include CacheReferences::PageCaching
-
- helper_method :cached_references
- attr_writer :cached_references
- alias_method_chain :render, :cache_reference_tracking
-
- class_inheritable_accessor :track_options
- self.track_options ||= {}
- end
-
- options = actions.extract_options!
- actions.map(&:to_sym).each do |action|
- self.track_options[action] = options[:track]
- end
- end
-
- def caches_page_with_references?
- method_defined? :caching_allowed_without_skipping
- end
-
- def tracks_cache_references?
- method_defined? :render_without_cache_reference_tracking
- end
- end
-
- def skip_caching!
- @skip_caching = true
- end
-
- def skip_caching?
- @skip_caching == true
- end
-
- protected
-
- def render_with_cache_reference_tracking(*args)
- options = args.last.is_a?(Hash) ? args.last : {}
- # skips caching if :skip_caching => true was passed or action is not configured to be cached
- skip_caching! if options.delete(:skip_caching) || !(track_options.has_key?(params[:action].to_sym))
-
- setup_method_call_tracking if track_method_calls?
- returning render_without_cache_reference_tracking(*args) do
- save_cache_references if track_method_calls?
- end
- end
-
- def track_method_calls?
- perform_caching and not skip_caching?
- end
-
- def setup_method_call_tracking
- @method_call_tracker ||= MethodCallTracking::Tracker.new
- @method_call_tracker.track(self, *method_call_trackables) # FIXME pass the controller when self === Component
- end
-
- def method_call_trackables
- trackables = self.class.track_options[params[:action].to_sym] || {}
- trackables.clone
- end
-
- def save_cache_references
- CachedPage.create_with_references(@site, @section, request.path, @method_call_tracker.references)
- end
-
- def caching_allowed_with_skipping
- caching_allowed_without_skipping && !skip_caching?
- end
- end
-end
-
-ActionController::Base.send :extend, CacheReferences::PageCaching::ActMacro
View
56 lib/cache_references/reference_tracking.rb
@@ -0,0 +1,56 @@
+require 'action_controller'
+require 'cache_references/method_call_tracking'
+
+module CacheReferences
+ module ReferenceTracking
+ TAGS_HEADER = 'rack-cache.tags'
+
+ module ActMacro
+ # Sets up reference tracking for given actions and objects
+ #
+ # tracks_references :index, :show, :track => [:article, :@articles, { :@site => :tag_counts }]
+ #
+ def tracks_references(*actions)
+ unless tracks_cache_references?
+ include ReferenceTracking
+
+ class_inheritable_accessor :reference_tracking_options
+ self.reference_tracking_options = { :header => TAGS_HEADER }
+ end
+
+ options = actions.extract_options!
+ actions.map(&:to_sym).each do |action|
+ self.reference_tracking_options[action] = options[:track]
+ end
+ end
+
+ def tracks_cache_references?
+ included_modules.include?(ReferenceTracking)
+ end
+ end
+
+ protected
+
+ def render(*)
+ setup_reference_tracking
+ result = super
+ add_reference_headers
+ result
+ end
+
+ def add_reference_headers
+ headers[reference_tracking_options[:header]] = @reference_tracker.tags
+ end
+
+ def setup_reference_tracking
+ @reference_tracker = MethodCallTracking::Tracker.new(self, reference_trackables)
+ end
+
+ def reference_trackables
+ trackables = reference_tracking_options[params[:action].to_sym] || {}
+ trackables.clone
+ end
+ end
+end
+
+ActionController::Base.send(:extend, CacheReferences::ReferenceTracking::ActMacro)
View
25 lib/cache_references/sweeper.rb
@@ -1,25 +0,0 @@
-module CacheReferences
- class Sweeper < ActionController::Caching::Sweeper
- def expire_cached_pages_by_site(site)
- expire_cached_pages site, CachedPage.find_all_by_site_id(site.id)
- end
-
- def expire_cached_pages_by_section(section)
- expire_cached_pages section, CachedPage.find_all_by_section_id(section.id)
- end
-
- def expire_cached_pages_by_reference(record, method = nil)
- expire_cached_pages record, CachedPage.find_by_reference(record, method)
- end
-
- def expire_cached_pages(record, pages)
- record.logger.warn cached_log_message_for(record, pages) if Site.cache_sweeper_logging
- controller.expire_pages(pages) if controller # TODO wtf ... why is controller sometimes nil here??
- end
-
- def cached_log_message_for(record, pages)
- msg = ["Expired pages referenced by #{record.class} ##{record.id}", "Expiring #{pages.size} page(s)"]
- pages.inject(msg) { |msg, page| msg << " - #{page.url}" }.join("\n")
- end
- end
-end
View
41 lib/cached_page.rb
@@ -1,41 +0,0 @@
-# Represents a cached page in the database. Has one or more references that expire it.
-
-class CachedPage < ActiveRecord::Base
- belongs_to :site
- validates_uniqueness_of :url, :scope => :site_id
-
- has_many :references, :class_name => "CachedPageReference", :dependent => :destroy
-
- class << self
- def find_by_reference(object, method = nil)
- sql = 'cached_page_references.object_type = ? AND cached_page_references.object_id = ?'
- sql << ' AND cached_page_references.method = ?' if method
-
- conditions = [sql, object.class.name, object.id]
- conditions << method.to_s if method
-
- find :all, :conditions => conditions, :include => :references
- end
-
- def create_with_references(site, section, url, references)
- returning find_or_initialize_by_site_id_and_url(site.id, url, :include => :references) do |page|
- [:compact!, :uniq!].each { |method| references.send method }
- references.each do |object, method|
- reference = CachedPageReference.initialize_with(object, method)
- page.references << reference unless page.references.detect {|r| r == reference }
- end
- page.section_id = section.id
- page.cleared_at = nil
- page.save!
- end
- end
-
- def expire_pages(pages)
- destroy pages.collect(&:id) unless pages.empty?
- end
-
- def delete_all_by_site_id(site_id)
- delete_all "site_id = #{site_id}"
- end
- end
-end
View
16 lib/cached_page_reference.rb
@@ -1,16 +0,0 @@
-class CachedPageReference < ActiveRecord::Base
- belongs_to :cached_page
-
- class << self
- def initialize_with(object, method = nil)
- new :object_type => object.class.name, :object_id => object.id, :method => method.to_s
- end
- end
-
- def ==(other)
- self.cached_page_id == other.cached_page_id &&
- self.object_type == other.object_type &&
- self.object_id == other.object_id &&
- self.method == other.method
- end
-end
View
54 test/cache_references_test.rb
@@ -1,54 +0,0 @@
-require File.expand_path(File.dirname(__FILE__) + '/test_helper')
-
-class CacheReferencesTest < Test::Unit::TestCase
- def setup
- @article = Article.new
- @comment = Comment.new
-
- @controller = ArticlesController.new
- @controller.stubs(:save_cache_references)
- @controller.instance_variable_set(:@article, @article)
- @controller.instance_variable_set(:@comments, [@comment])
- end
-
- def tracker
- @controller.instance_variable_get(:@method_call_tracker)
- end
-
- def test_access_to_an_attribute_on_an_observed_object_records_the_reference
- @controller.send :render
- @article.title
- assert tracker.references.include?([@article, :read_attribute])
- end
-
- def test_access_to_a_registered_method_on_an_observed_object_records_the_reference
- @controller.send :render
- @article.section
- assert tracker.references.include?([@article, :section])
- end
-
- def test_access_to_an_attribute_on_an_observed_array_of_objects_records_the_reference
- @controller.send :render
- @comment.body
- assert tracker.references.include?([@comment, :read_attribute])
- end
-
- def test_access_to_a_registered_method_on_an_observed_array_of_objects_records_the_reference
- @controller.send :render
- @comment.section
- assert tracker.references.include?([@comment, :section])
- end
-
- def test_does_not_setup_method_call_tracking_if_skip_caching_is_passed_as_option
- @controller.send :render, :skip_caching => true
- @article.title
- assert_equal nil, tracker
- end
-
- def test_does_not_setup_method_call_tracking_if_skip_caching_is_called_on_controller
- @controller.skip_caching!
- @controller.send :render
- @article.title
- assert_equal nil, tracker
- end
-end
View
145 test/method_call_tracking_test.rb
@@ -1,108 +1,105 @@
-$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
+require File.expand_path('../test_helper', __FILE__)
-require 'rubygems'
-require 'mocha'
-require 'cache_references/method_call_tracking'
-
-class MethodCallTrackerTest < Test::Unit::TestCase
+class MethodCallTrackingTest < Test::Unit::TestCase
include CacheReferences
+ attr_reader :object, :tracker, :references
+
+ class Object
+ include CacheReferences::MethodCallTracking
+
+ def title; end
+ def foo(bar, baz); end
+ end
+
def setup
- @controller = mock('controller')
@tracker = MethodCallTracking::Tracker.new
+ @object = Object.new
+ @references = []
end
- def test_resolve_trackable_resolves_ivars_and_method_names_given_as_symbols_or_strings
- @controller.expects(:instance_variable_get).with(:@foo)
- @tracker.send(:resolve_trackable, @controller, :@foo)
-
- @controller.expects(:instance_variable_get).with(:@foo)
- @tracker.send(:resolve_trackable, @controller, '@foo')
-
- @controller.expects(:foo)
- @tracker.send(:resolve_trackable, @controller, :foo)
-
- @controller.expects(:foo)
- @tracker.send(:resolve_trackable, @controller, 'foo')
+ test 'Tracker#resolve_trackable resolves ivars given as symbols' do
+ object.expects(:instance_variable_get).with(:@foo)
+ tracker.send(:resolve_trackable, object, :@foo)
end
-
- def test_initialize_resolves_trackables_given_as_symbol_string_or_hash_key
- @controller.expects(:instance_variable_get).with(:@foo)
- @controller.expects(:bar)
- @controller.expects(:baz)
-
- @tracker.track @controller, :@foo, :bar, { :baz => nil }
+
+ test 'Tracker#resolve_trackable resolves ivars given as strings' do
+ object.expects(:instance_variable_get).with(:@foo)
+ tracker.send(:resolve_trackable, object, '@foo')
end
-
- def test_tracks_read_attribute_method_when_given_method_is_an_attribute
- foo = stub('foo', :has_attribute? => true)
- @controller.expects(:foo).returns foo
-
- foo.expects(:track_method_calls).with([], :read_attribute)
- @tracker.track @controller, :foo
+
+ test 'Tracker#resolve_trackable resolves method_names given as symbols' do
+ object.expects(:foo)
+ tracker.send(:resolve_trackable, object, :foo)
end
-
- def test_tracks_method_when_given_method_is_not_an_attribute
- foo = stub('foo', :has_attribute? => false)
- @controller.expects(:foo).returns foo
-
- foo.expects(:track_method_calls).with([], :bar)
- @tracker.track @controller, :foo => :bar
+
+ test 'Tracker#resolve_trackable resolves method_names given as strings' do
+ object.expects(:foo)
+ tracker.send(:resolve_trackable, object, 'foo')
end
-end
-class MethodReadTrackingTest < Test::Unit::TestCase
- class Record
- include CacheReferences::MethodCallTracking
+ test 'Tracker#track resolves trackables given as a symbol, string or hash key' do
+ [:@foo, :@bar].each { |ivar| object.expects(:instance_variable_get).with(ivar) }
+ [:baz, :buz, :bum].each { |method| object.expects(method) }
- def title; end
- def foo(bar, baz); end
+ tracker.track(object, [:@foo, '@bar', :baz, 'buz', { :bum => nil }])
end
- def setup
- @record = Record.new
- @references = []
+ test 'Tracker#track tracks read_attribute method when the given method is an attribute' do
+ foo = stub('foo', :has_attribute? => true)
+ object.expects(:foo).returns(foo)
+
+ foo.expects(:track_method_calls).with([], [:read_attribute])
+ tracker.track(object, [:foo])
+ end
+
+ test 'Tracker#track tracks the given method when it is not an attribute' do
+ foo = stub('foo', :has_attribute? => false)
+ object.expects(:foo).returns(foo)
+
+ foo.expects(:track_method_calls).with([], [:bar])
+ tracker.track(object, [{ :foo => :bar }])
end
- def test_installs_on_methods_without_arguments
+ test 'track_method_calls installs on methods that do no take any arguments' do
assert_nothing_raised {
- @record.track_method_calls(@references, :title)
- @record.title
+ object.track_method_calls(references, [:title])
+ object.title
}
end
- def test_installs_on_methods_with_arguments
+ test 'track_method_calls installs on methods that take arguments' do
assert_nothing_raised {
- @record.track_method_calls(@references, :foo)
- @record.foo(:bar, :baz)
+ object.track_method_calls(references, [:foo])
+ object.foo(:bar, :baz)
}
end
- def test_installs_method_on_metaclass
- @record.track_method_calls(@references, :title)
- assert (class << @record; self; end).method_defined?(:title)
+ test "track_method_calls installs a new method on the object's meta class" do
+ object.track_method_calls(references, [:title])
+ assert (class << object; self; end).method_defined?(:title)
end
- def test_adds_method_call_reference_to_given_references_array
- @record.track_method_calls(@references, :title)
- @record.title
- assert_equal [@record, :title], @references.first
+ test "a call to a tracked method adds a reference to the given references array" do
+ object.track_method_calls(references, [:title])
+ object.title
+ assert_equal [object, :title], references.first
end
- def test_does_not_add_multiple_references_on_subsequent_calls
- @record.track_method_calls(@references, :title)
- @record.title
- @record.title
- assert_equal [@record, :title], @references.first
+ test "subsequent calls to the same tracked method do not add multiple references" do
+ object.track_method_calls(references, [:title])
+ object.title
+ object.title
+ assert_equal [object, :title], references.first
end
-
- def test_allows_to_setup_tracking_multiple_times_for_the_same_method
+
+ test "tracking can be set up multiple times for the same method" do
assert_nothing_raised {
- @record.track_method_calls(@references, :title)
- @record.track_method_calls(@references, :title)
+ object.track_method_calls(references, [:title])
+ object.track_method_calls(references, [:title])
}
- @record.title
- @record.title
- assert_equal [@record, :title], @references.first
+ object.title
+ object.title
+ assert_equal [object, :title], references.first
end
end
View
43 test/reference_tracking_test.rb
@@ -0,0 +1,43 @@
+require File.expand_path('../test_helper', __FILE__)
+
+class ReferenceTrackingTest < Test::Unit::TestCase
+ attr_reader :controller, :article, :comment
+
+ def setup
+ @article = Article.new
+ @comment = Comment.new
+
+ @controller = ArticlesController.new
+ @controller.instance_variable_set(:@article, @article)
+ @controller.instance_variable_set(:@comments, [@comment])
+ end
+
+ def tracker
+ controller.instance_variable_get(:@reference_tracker)
+ end
+
+ test 'accessing an attribute on an observed object records the reference' do
+ controller.process { article.title }
+ assert tracker.references.include?([article, :read_attribute])
+ end
+
+ test 'accessing a registered method on an observed object records the reference' do
+ controller.process { article.section }
+ assert tracker.references.include?([article, :section])
+ end
+
+ test 'accessing an attribute on an observed array of objects records the reference' do
+ controller.process { comment.body }
+ assert tracker.references.include?([comment, :read_attribute])
+ end
+
+ test 'accessing a registered method on an observed array of objects records the reference' do
+ controller.process { comment.section }
+ assert tracker.references.include?([comment, :section])
+ end
+
+ test 'adds reference tags to the headers hash' do
+ controller.process { article.title; comment.section; comment.body }
+ assert_equal 'article-1,comment-2', controller.headers[CacheReferences::ReferenceTracking::TAGS_HEADER]
+ end
+end
View
38 test/test_helper.rb
@@ -1,33 +1,41 @@
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
require 'rubygems'
-require 'actionpack'
-require 'action_controller'
+require 'test/unit'
require 'mocha'
+require 'test_declarative'
-require 'cache_references/page_caching'
+require 'cache_references/reference_tracking'
+require 'cache_references'
+
+class ActionController::Base
+ def render(*)
+ yield
+ end
+end
class ArticlesController < ActionController::Base
- caches_page_with_references :show, :track => [:@article, :@comments, { :@article => :section, :@comments => :section }]
-
+ tracks_references :show, :track => [:@article, :@comments, { :@article => :section, :@comments => :section }]
+
+ def process(&block)
+ self.response = ActionDispatch::Response.new
+ run_callbacks(:process_action, :show) { render(&block) }
+ end
+
def params
{ :action => :show }
end
-
- def render_without_cache_reference_tracking(*args)
- end
end
class Record
include CacheReferences::MethodCallTracking
- def section
- end
+ def section; end
def read_attribute(name)
@attributes[name]
end
-
+
def method_missing(name)
read_attribute(name)
end
@@ -37,11 +45,19 @@ class Article < Record
def initialize
@attributes = {:title => '', :body => ''}
end
+
+ def id
+ 1
+ end
end
class Comment < Record
def initialize
@attributes = {:body => ''}
end
+
+ def id
+ 2
+ end
end
Please sign in to comment.
Something went wrong with that request. Please try again.