Browse files

first commit

  • Loading branch information...
0 parents commit acd93cec48ebedfa026e058835e6d9e9e2a4f093 @hellvinz committed Jan 15, 2009
Showing with 260 additions and 0 deletions.
  1. +20 −0 MIT-LICENSE
  2. +35 −0 README
  3. +23 −0 Rakefile
  4. +1 −0 init.rb
  5. +13 −0 install.rb
  6. +9 −0 lib/varnish.rb
  7. +40 −0 lib/varnish/esi.rb
  8. +25 −0 lib/varnish/expire_cache.rb
  9. +35 −0 lib/varnish/utils.rb
  10. +4 −0 tasks/varnish_tasks.rake
  11. +6 −0 test/test_helper.rb
  12. +47 −0 test/varnish_test.rb
  13. +2 −0 uninstall.rb
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 [Hellvinz]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 README
@@ -0,0 +1,35 @@
+Varnish
+=======
+
+This plugin provides helpers to interract with the Varnish proxy.
+It aims at covering two parts:
+- the purge of a cached page/action.
+- the use of ESI includes from Rails (within the light scope of varnish esi support)
+
+
+Example
+=======
+
+- Remotely expire page '/example/test' from a controller/sweeper (happens to expire also the cached page if rails cache enabled)
+expire_page('/example/test')
+
+- Remotely expire action '/example/test' from a controller/sweeper (happens to expire also the cached action if rails cache enabled)
+expire_action('/example/test')
+
+- Adding an ESI include with a time to live of 10 seconds in a Rails view
+<%= render :esi => time_now_path(:format => :esi), :ttl => 10 %>
+
+Setup
+======
+
+script/install git://github.com/hellvinz/varnish_rails.git
+
+Edit RAILS_ROOT/config/varnish.yml to reflect your varnish installation
+
+Adapt your varnish installation to allow:
+- purge from the rails host (http://varnish.projects.linpro.no/wiki/VCLExamplePurging)
+- esi http://varnish.projects.linpro.no/wiki/ESIfeatures
+
+That's all.
+
+Copyright (c) 2009 [hellvinz], released under the MIT license
23 Rakefile
@@ -0,0 +1,23 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the varnish plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.libs << 'test'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the varnish plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Varnish'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
1 init.rb
@@ -0,0 +1 @@
+require 'varnish'
13 install.rb
@@ -0,0 +1,13 @@
+require 'yaml'
+
+defaults = {:varnish => {:ip => '127.0.0.1', :port => 80}}
+
+config_dest = "./config/varnish.yml"
+
+File.open(config_dest, 'w') { |f| f.write(YAML.dump(defaults)) }
+if File.exists?(config_dest)
+ puts "File: #{config_dest} already exists, would you like to overide it? (Y/n)"
+ exit if [110,78].include? STDIN.getc
+end
+
+puts "Creating varnish.yml config file to #{config_dest}"
9 lib/varnish.rb
@@ -0,0 +1,9 @@
+require 'varnish/expire_cache'
+require 'varnish/esi'
+
+Mime::Type.register_alias 'text/html', :esi
+
+ActionController::Base.send(:include, Varnish::CacheAction)
+ActionController::Base.send(:include, Varnish::CachePage)
+ActionController::Base.send(:include, Varnish::EsiExpire)
+ActionView::Base.send(:include, Varnish::RenderWithEsi)
40 lib/varnish/esi.rb
@@ -0,0 +1,40 @@
+module Varnish
+
+ module EsiExpire
+ def self.included(base)
+ base.class_eval do
+ # the max-age esi parameter is ignored by varnish
+ # buts it does care about the max-age of the http header
+ # so we pass it to the esi request
+ append_after_filter{|controller| controller.response.headers["Cache-Control"] = "max-age=#{controller.params[:ttl]},public,must-revalidate" if controller.request.format == :esi}
+ end
+ end
+ end
+
+ # this module comes from fragment_fu
+ # http://mongrel-esi.googlecode.com/svn/trunk/plugin/fragment_fu/
+ module RenderWithEsi
+ def self.included(base)
+ base.class_eval do
+ alias_method_chain :render, :esi_option
+ end
+ end
+
+ def render_with_esi_option(options = {}, old_local_assigns = {}, &block)
+ if options[:esi]
+ render_esi(options)
+ else
+ render_without_esi_option(options, old_local_assigns, &block)
+ end
+ end
+
+ def render_esi(options)
+ url = options[:esi]
+ query = (url.is_a?(Hash) ? controller.url_for(url.merge({:only_path => true})) : url.gsub(/\.\w+?$/,''))
+ # the max-age esi parameter is ignored by varnish
+ # buts it does care about the max-age of the http header
+ # so we pass it to the esi request
+ %Q{<esi:include src="#{query}.esi?ttl=#{options[:ttl].to_i || 0}" max-age="#{options[:ttl].to_i || 0}"/>}
+ end
+ end
+end
25 lib/varnish/expire_cache.rb
@@ -0,0 +1,25 @@
+require 'varnish/utils'
+
+module Varnish
+ module CachePage
+ def self.included(base)
+ base.send(:alias_method_chain, :expire_page, :varnish)
+ end
+
+ def expire_page_with_varnish(path)
+ VarnishUtils.instance.purge(request.host,path)
+ expire_page_without_varnish(path)
+ end
+ end
+
+ module CacheAction
+ def self.included(base)
+ base.send(:alias_method_chain, :expire_action, :varnish)
+ end
+
+ def expire_action_with_varnish(path)
+ VarnishUtils.instance.purge(request.host,path)
+ expire_action_without_varnish(path)
+ end
+ end
+end
35 lib/varnish/utils.rb
@@ -0,0 +1,35 @@
+require 'socket'
+require 'yaml'
+require 'uri'
+
+class VarnishBadConfiguration < Exception;;end
+
+class VarnishUtils
+ include Singleton
+
+ CONFIG_FILE = "#{RAILS_ROOT}/config/varnish.yml"
+
+ def initialize
+ @configured = false
+ config = YAML.load_file(CONFIG_FILE)
+ @varnish_ip = config[:varnish][:ip]
+ @varnish_port = config[:varnish][:port]
+ @configured = true
+ rescue => e
+ puts "Check your config file: #{CONFIG_FILE}"
+ raise VarnishBadConfiguration
+ end
+
+ def purge(domain, uri)
+ return unless configured?
+ socket = TCPSocket.new( @varnish_ip, @varnish_port)
+ socket.write "PURGE #{uri} HTTP/1.1\r\nUser-Agent: ruby/socket\r\nAccept: */*\r\nHost: #{domain}\r\n\r\n"
+ socket.close
+ end
+
+ private
+ def configured?
+ @configured ? true : raise(VarnishBadConfiguration,"Check your config file: #{CONFIG_FILE}")
+ end
+
+end
4 tasks/varnish_tasks.rake
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :varnish do
+# # Task goes here
+# end
6 test/test_helper.rb
@@ -0,0 +1,6 @@
+require 'rubygems'
+require "mocha"
+require "test/spec"
+require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
+require 'varnish'
+$LOAD_PATH << '../'
47 test/varnish_test.rb
@@ -0,0 +1,47 @@
+require 'test_helper'
+
+context 'plugin installation' do
+ specify 'it should write the configuration file' do
+ File.expects(:open).with("./config/varnish.yml",'w')
+ require 'install.rb'
+ end
+end
+
+context 'purge varnish' do
+ specify 'it should alias expire_page and expire_action' do
+ ActionController::Base.expects(:alias_method_chain).with(:expire_action, :varnish)
+ ActionController::Base.expects(:alias_method_chain).with(:expire_page, :varnish)
+ load 'lib/varnish.rb'
+ end
+
+ specify 'it should raise when config file is malformed' do
+ ac = ActionController::Base.new
+ YAML.expects(:load_file).returns({})
+ assert_raise VarnishBadConfiguration do
+ ac.expire_page('/bla')
+ end
+ end
+
+ specify 'it should open a socket to varnish to PURGE /bla on the current host' do
+ ac = ActionController::Base.new
+ ac.request.expects(:host).returns('test_host')
+ YAML.expects(:load_file).returns({:varnish => {:ip => '127.0.0.1', :port => 80}})
+ ts = TCPSocket.new('127.0.0.1',80)
+ TCPSocket.expects(:new).with('127.0.0.1',80).returns(ts)
+ ts.expects(:write).with("PURGE /bla HTTP/1.1\r\nUser-Agent: ruby/socket\r\nAccept: */*\r\nHost: test_host\r\n\r\n")
+ ac.expire_page('/bla')
+ end
+end
+
+
+context 'edge side includes' do
+ specify 'it should alias render to render_with_esi_option' do
+ ActionView::Base.expects(:alias_method_chain).with(:render, :esi_option)
+ load 'lib/varnish.rb'
+ end
+
+ specify 'it should create an esi include anchor' do
+ av = ActionView::Base.new
+ av.render(:esi => '/bla').should.equal "<esi:include src=\"/bla.esi?ttl=0\" max-age=\"0\"/>"
+ end
+end
2 uninstall.rb
@@ -0,0 +1,2 @@
+require 'FileUtils'
+FileUtils.rm './config/varnish.yml'

0 comments on commit acd93ce

Please sign in to comment.