Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

initial commit

  • Loading branch information...
commit dd1c6b0e6951429dc0e37a6e9404a6be204a51f3 0 parents
Sven Fuchs authored
36  lib/rack/cache/tags.rb
... ...
@@ -0,0 +1,36 @@
  1
+require 'uri'
  2
+
  3
+require 'rack/cache'
  4
+require 'rack/cache/storage'
  5
+require 'rack/cache/utils'
  6
+
  7
+require 'rack/cache/tags/context'
  8
+require 'rack/cache/tags/storage'
  9
+require 'rack/cache/tags/tag_store'
  10
+require 'rack/cache/tags/meta_store'
  11
+
  12
+module Rack::Cache
  13
+  TAGS_HEADER       = 'X-Cache-Tags'
  14
+  PURGE_TAGS_HEADER = 'X-Cache-Purge-Tags'
  15
+
  16
+  Context.class_eval do
  17
+    option_accessor :tagstore
  18
+
  19
+    def tagstore
  20
+      uri = options['rack-cache.tagstore']
  21
+      storage.resolve_tagstore_uri(uri)
  22
+    end
  23
+  end
  24
+
  25
+  module Tags
  26
+    class << self
  27
+      def new(backend, options={}, &b)
  28
+        Context.new(backend, options, &b)
  29
+      end
  30
+
  31
+      def normalize(tags)
  32
+        Array(tags).join(',').split(',').map { |tag| tag.strip }
  33
+      end
  34
+    end
  35
+  end
  36
+end
48  lib/rack/cache/tags/context.rb
... ...
@@ -0,0 +1,48 @@
  1
+module Rack::Cache::Tags
  2
+  class Context
  3
+    def initialize(backend, options = {})
  4
+      @backend = backend
  5
+      @options = options
  6
+      yield self if block_given?
  7
+    end
  8
+
  9
+    def call(env)
  10
+      @env     = env
  11
+      request  = Rack::Cache::Request.new(env)
  12
+      response = Rack::Cache::Response.new(*@backend.call(env))
  13
+      
  14
+      tags = response.headers.delete(Rack::Cache::PURGE_TAGS_HEADER)
  15
+      if tags
  16
+        tags = Rack::Cache::Tags.normalize(tags)
  17
+        uris = tagged_uris(tags)
  18
+        response.headers[Rack::Cache::PURGE_HEADER] = uris.join("\n")
  19
+        
  20
+        purge_taggings(request, uris)
  21
+      end
  22
+
  23
+      response.to_a
  24
+    end
  25
+
  26
+    protected
  27
+      
  28
+      def tagged_uris(tags)
  29
+        tags.inject([]) { |uris, tag| uris + tagstore.by_tag[tag] }
  30
+      end
  31
+      
  32
+      def purge_taggings(request, uris)
  33
+        uris.each do |uri|
  34
+          key = Rack::Cache::Utils::Key.call(request, uri)
  35
+          tagstore.purge(key)
  36
+        end
  37
+      end
  38
+    
  39
+      def tagstore
  40
+        uri = @env['rack-cache.tagstore']
  41
+        storage.resolve_tagstore_uri(uri)
  42
+      end
  43
+    
  44
+      def storage
  45
+        Rack::Cache::Storage.instance
  46
+      end
  47
+  end
  48
+end
19  lib/rack/cache/tags/meta_store.rb
... ...
@@ -0,0 +1,19 @@
  1
+Rack::Cache::MetaStore::Heap.class_eval do
  2
+  def store(request, response, entity_store)
  3
+    key = super
  4
+
  5
+    tags = response.headers[Rack::Cache::TAGS_HEADER]
  6
+    tagstore(request).store(key, tags) if tags
  7
+    
  8
+    key
  9
+  end
  10
+  
  11
+  def tagstore(request)
  12
+    uri = request.env['rack-cache.tagstore']
  13
+    storage.resolve_tagstore_uri(uri)
  14
+  end
  15
+
  16
+  def storage
  17
+    Rack::Cache::Storage.instance
  18
+  end
  19
+end
6  lib/rack/cache/tags/storage.rb
... ...
@@ -0,0 +1,6 @@
  1
+Rack::Cache::Storage.class_eval do
  2
+  def resolve_tagstore_uri(uri)
  3
+    @tagstores ||= {}
  4
+    @tagstores[uri.to_s] ||= create_store(Rack::Cache::Tags::TagStore, uri)
  5
+  end
  6
+end
75  lib/rack/cache/tags/tag_store.rb
... ...
@@ -0,0 +1,75 @@
  1
+require 'fileutils'
  2
+require 'digest/sha1'
  3
+require 'rack/utils'
  4
+
  5
+module Rack::Cache::Tags
  6
+  class TagStore
  7
+    def store(key, tags)
  8
+      tags = Rack::Cache::Tags.normalize(tags)
  9
+      write(key, tags)
  10
+    end
  11
+  
  12
+    def by_key(key)
  13
+      raise NotImplemented
  14
+    end
  15
+
  16
+    def by_tag(tag)
  17
+      raise NotImplemented
  18
+    end
  19
+
  20
+  protected
  21
+
  22
+    def write(key, tags)
  23
+      raise NotImplemented
  24
+    end
  25
+
  26
+    def purge(key)
  27
+      raise NotImplemented
  28
+    end
  29
+
  30
+  public
  31
+
  32
+    class Heap < TagStore
  33
+      def self.resolve(uri)
  34
+        new
  35
+      end
  36
+      
  37
+      def initialize
  38
+        @hash = { :by_key => {}, :by_tag => {} }
  39
+      end
  40
+      
  41
+      def by_key
  42
+        @hash[:by_key]
  43
+      end
  44
+      
  45
+      def by_tag
  46
+        @hash[:by_tag]
  47
+      end
  48
+
  49
+      def purge(key)
  50
+        if tags = by_key[key]
  51
+          tags.each do |tag|
  52
+            next unless by_tag[tag]
  53
+            by_tag[tag].delete(key) 
  54
+            by_tag.delete(tag) if by_tag[tag].empty?
  55
+          end
  56
+        end
  57
+        
  58
+        by_key.delete(key)
  59
+        nil
  60
+      end
  61
+
  62
+      def write(key, tags)
  63
+        by_key[key] = tags
  64
+
  65
+        tags.each do |tag|
  66
+          by_tag[tag] ||= []
  67
+          by_tag[tag] << key unless by_tag[tag].include?(key)
  68
+        end
  69
+      end
  70
+    end
  71
+
  72
+    HEAP = Heap
  73
+    MEM = HEAP
  74
+  end
  75
+end
3  test/all.rb
... ...
@@ -0,0 +1,3 @@
  1
+Dir[File.dirname(__FILE__) + '/**/*_test.rb'].each do |filename|
  2
+  require filename
  3
+end
43  test/tag_store_test.rb
... ...
@@ -0,0 +1,43 @@
  1
+require "#{File.dirname(__FILE__)}/test_setup"
  2
+
  3
+describe 'Rack::Cache::Tags::Tagstore' do
  4
+  before(:each) do 
  5
+    @store = Rack::Cache::Tags::TagStore::Heap.new
  6
+  end
  7
+  
  8
+  it "can be resolved from an uri" do
  9
+    tag_store = Rack::Cache::Storage.new.resolve_tagstore_uri('heap:/')
  10
+    tag_store.should.be.kind_of Rack::Cache::Tags::TagStore::Heap
  11
+  end
  12
+
  13
+  it "writes to both by_key and by_tag" do
  14
+    @store.store('1234', 'tag-1,tag-2')
  15
+
  16
+    @store.by_key['1234'].should  == ['tag-1', 'tag-2']
  17
+    @store.by_tag['tag-1'].should == ['1234']
  18
+    @store.by_tag['tag-2'].should == ['1234']
  19
+    @store.by_tag['tag-3'].should == nil
  20
+
  21
+    @store.store('5678', 'tag-1,tag-3')
  22
+    
  23
+    @store.by_key['1234'].should  == ['tag-1', 'tag-2']
  24
+    @store.by_key['5678'].should  == ['tag-1', 'tag-3']
  25
+    @store.by_tag['tag-1'].should == ['1234', '5678']
  26
+    @store.by_tag['tag-2'].should == ['1234']
  27
+    @store.by_tag['tag-3'].should == ['5678']
  28
+  end
  29
+  
  30
+  it "purges from both by_key and by_tag" do
  31
+    @store.store('1234', 'tag-1,tag-2')
  32
+    @store.store('5678', 'tag-1,tag-3')
  33
+    
  34
+    @store.purge('1234')
  35
+    
  36
+    @store.by_key['1234'].should  == nil
  37
+    @store.by_key['5678'].should  == ['tag-1', 'tag-3']
  38
+    @store.by_tag['tag-1'].should == ['5678']
  39
+    @store.by_tag['tag-2'].should == nil
  40
+    @store.by_tag['tag-3'].should == ['5678']
  41
+  end
  42
+  
  43
+end
43  test/tags_test.rb
... ...
@@ -0,0 +1,43 @@
  1
+require "#{File.dirname(__FILE__)}/test_setup"
  2
+
  3
+describe 'Rack::Cache::Tags' do
  4
+  before(:each) { setup_cache_context }
  5
+  after(:each)  { teardown_cache_context }
  6
+
  7
+  it "writes tags for a key on :store" do
  8
+    respond_with 200, { 'X-Cache-Tags' => 'page-1,user-2', 'Cache-Control' => 'public, max-age=10000' }, 'body'
  9
+
  10
+    response = get '/'
  11
+    tagstore.by_key.should == { 'http://example.org/' => ['page-1', 'user-2'] }
  12
+    tagstore.by_tag.should == { 'page-1' => ['http://example.org/'], 'user-2' => ['http://example.org/'] }
  13
+  end
  14
+
  15
+  it "deletes tags for a key on :purge" do
  16
+    respond_with 200, { 'Cache-Control' => 'public, max-age=10000' }, 'body'
  17
+    respond_with 200, { 'Cache-Control' => 'public, max-age=500' }, 'body' do |req, res|
  18
+      case req.path
  19
+      when '/'
  20
+        res.headers['X-Cache-Tags'] = ['page-1,user-2']
  21
+      when '/users/2'
  22
+        res.headers['X-Cache-Purge-Tags'] = 'user-2'
  23
+      end
  24
+    end
  25
+
  26
+    get '/'
  27
+    tagstore.by_key['http://example.org/'].should.include 'user-2'
  28
+    cache.trace.should.include :store
  29
+
  30
+    get '/'
  31
+    cache.trace.should.include :fresh
  32
+
  33
+    post '/users/2'
  34
+    tagstore.by_key['http://example.org/'].should.be.nil
  35
+
  36
+    get '/'
  37
+    cache.trace.should.include :miss
  38
+  end
  39
+
  40
+  def tagstore
  41
+    cache.send(:tagstore)
  42
+  end
  43
+end
124  test/test_setup.rb
... ...
@@ -0,0 +1,124 @@
  1
+# test setup largely stolen from Ryan Tomayko's rack-cache
  2
+
  3
+require 'pp'
  4
+
  5
+begin
  6
+  require 'test/spec'
  7
+rescue LoadError => boom
  8
+  require 'rubygems' rescue nil
  9
+  require 'test/spec'
  10
+end
  11
+
  12
+# Setup the load path ..
  13
+$: << File.dirname(File.dirname(__FILE__)) + '/../rack-cache/lib'
  14
+$: << File.dirname(File.dirname(__FILE__)) + '/../rack-cache-purge/lib'
  15
+$: << File.dirname(File.dirname(__FILE__)) + '/lib'
  16
+$: << File.dirname(__FILE__)
  17
+
  18
+require 'rack/cache'
  19
+require 'rack/cache/purge'
  20
+require 'rack/cache/tags'
  21
+
  22
+# Methods for constructing downstream applications / response
  23
+# generators.
  24
+module CacheContextHelpers
  25
+  attr_reader :app, :cache, :tags
  26
+
  27
+  def setup_cache_context
  28
+    # holds each Rack::Cache::Context
  29
+    @app = nil
  30
+
  31
+    # each time a request is made, a clone of @cache_template is used
  32
+    # and appended to @caches.
  33
+    @cache_template = nil
  34
+    @cache = nil
  35
+    @caches = []
  36
+    @errors = StringIO.new
  37
+    @cache_config = nil
  38
+
  39
+    @called = false
  40
+    @request = nil
  41
+    @response = nil
  42
+    @responses = []
  43
+
  44
+    @storage = Rack::Cache::Storage.instance
  45
+  end
  46
+
  47
+  def teardown_cache_context
  48
+    @app, @cache_template, @cache, @caches, @called,
  49
+    @request, @response, @responses, @cache_config = nil
  50
+    @storage.clear
  51
+  end
  52
+
  53
+  # A basic response with 200 status code and a tiny body.
  54
+  def respond_with(status=200, headers={}, body=['Hello World'])
  55
+    called = false
  56
+    @app = lambda do |env|
  57
+      called = true
  58
+      response = Rack::Response.new(body, status, headers)
  59
+      request = Rack::Request.new(env)
  60
+      yield request, response if block_given?
  61
+      response.finish
  62
+    end
  63
+    @app.meta_def(:called?) { called }
  64
+    @app.meta_def(:reset!) { called = false }
  65
+    @app
  66
+  end
  67
+
  68
+  def cache_config(&block)
  69
+    @cache_config = block
  70
+  end
  71
+
  72
+  def request(method, uri = '/', env = {})
  73
+    fail 'response not specified (use respond_with)' if @app.nil?
  74
+    @app.reset! if @app.respond_to?(:reset!)
  75
+
  76
+    @tags    = Rack::Cache::Tags.new(@app)
  77
+    @purge   = Rack::Cache::Purge.new(@tags, :allow_http_purge => true)
  78
+    @cache   = Rack::Cache::Context.new(@purge, :tagstore => 'heap:/', &@cache_config)
  79
+    @request = Rack::MockRequest.new(@cache)
  80
+
  81
+    env = { 'rack.run_once' => true }.merge(env)
  82
+
  83
+    @response = @request.request(method.to_s.upcase, uri, env)
  84
+    @responses << @response
  85
+    @response
  86
+  end
  87
+
  88
+  def get(stem, env={}, &b)
  89
+    request(:get, stem, env, &b)
  90
+  end
  91
+
  92
+  def head(stem, env={}, &b)
  93
+    request(:head, stem, env, &b)
  94
+  end
  95
+
  96
+  def post(*args, &b)
  97
+    request(:post, *args, &b)
  98
+  end
  99
+end
  100
+
  101
+class Test::Unit::TestCase
  102
+  include CacheContextHelpers
  103
+end
  104
+
  105
+# Metaid == a few simple metaclass helper
  106
+# (See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html.)
  107
+class Object
  108
+  # The hidden singleton lurks behind everyone
  109
+  def metaclass; class << self; self; end; end
  110
+  def meta_eval(&blk); metaclass.instance_eval(&blk); end
  111
+  # Adds methods to a metaclass
  112
+  def meta_def name, &blk
  113
+    meta_eval { define_method name, &blk }
  114
+  end
  115
+  # Defines an instance method within a class
  116
+  def class_def name, &blk
  117
+    class_eval { define_method name, &blk }
  118
+  end
  119
+
  120
+  # True when the Object is neither false or nil.
  121
+  def truthy?
  122
+    !!self
  123
+  end
  124
+end

0 notes on commit dd1c6b0

Please sign in to comment.
Something went wrong with that request. Please try again.