Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

add DiskBackedMemory store; other small tweaks and cleanup

  • Loading branch information...
commit 825fb93e0351c7ee67727bfcf3851c2c873027c2 1 parent 180c835
@rtomayko authored
View
5 .autotest
@@ -1,8 +1,7 @@
-
-
Autotest.add_hook :initialize do |t|
t.clear_mappings
- t.add_exception(/^(?:\/.)?\.git.*/)
+ t.add_exception(/^(?:\/\.)?\.git.*/)
+ t.add_exception(/^(?:\/\.)?\[\..*/)
t.add_mapping(/^lib\/.*\.rb$/) { |fn, _|
t.files_matching(/^test\/#{File.basename(fn, '.rb')}_test.rb$/) }
t.add_mapping(/^test\/.*_test\.rb$/) { |fn, _| fn }
View
2  lib/rack/cache/config/default.rb
@@ -77,7 +77,7 @@
on :deliver do
# Handle conditional GET w/ If-Modified-Since
- if @response.not_modified?(@request.header['If-Modified-Since'])
+ if @response.last_modified_at?(@request.header['If-Modified-Since'])
debug 'upstream version is unmodified; sending 304'
response.status = 304
response.body = ''
View
75 lib/rack/cache/default.rb
@@ -1,75 +0,0 @@
-# Forward the request to the backend and call finish to send the
-# response back upstream.
-on :pass do
- status, header, body = @backend.call(@request.env)
- @backend_response = Response.new(body, status, header)
- @response = @backend_response
- finish
-end
-
-# Called when the request is initially received.
-on :receive do
- # TODO actual Cache-Control parsing
- pass if request.header['Cache-Control'] =~ /no-cache/
- pass unless request.method? 'GET', 'HEAD'
- pass if request.header? 'Cookie', 'Authorization', 'Expect'
- lookup
-end
-
-# Attempt to lookup the request in the cache. If a potential
-# response is found, control transfers to the hit event with
-# @object set to response object retrieved from cache. When
-# no object is found in the cache, control transfers to miss.
-on :lookup do
- if object = @storage.get(request.fullpath)
- @object = Response.activate(object)
- hit if @object.fresh?
- end
- miss
-end
-
-# The cache hit after a lookup.
-on :hit do
- @response = @object
- deliver
-end
-
-# Nothing was found in the cache.
-on :miss do
- fetch
-end
-
-# Fetch the response from the backend and transfer control
-# to the store event.
-on :fetch do
- status, header, body = @backend.call(@backend_request.env)
- @backend_response = Response.new(body, status, header)
- if @backend_response.cacheable?
- @response = @backend_response.dup
- @response.extend Cacheable
- store
- else
- @response = @backend_response
- deliver
- end
-end
-
-# Store the response in the cache and transfer control to
-# the deliver event.
-on :store do
- @object = @response
- @storage.put(@request.fullpath, @object.persist)
- deliver
-end
-
-on :deliver do
- finish
-end
-
-
-# Complete processing of the request. The backend_request,
-# backend_response, and response objects should all be available
-# when this event is invoked.
-on :finish do
- throw :finish, @response.finish
-end
View
7 lib/rack/cache/response.rb
@@ -83,7 +83,7 @@ def last_modified
headers['Last-Modified']
end
- def not_modified?(date)
+ def last_modified_at?(date)
date && last_modified == date
end
@@ -94,6 +94,11 @@ def cacheable?
!(no_store? || no_cache?)
end
+ def validateable?
+ # TODO support ETags
+ cacheable? && header?('Last-Modified')
+ end
+
end
View
133 lib/rack/cache/storage.rb
@@ -1,16 +1,30 @@
-module Rack::Cache
+require 'enumerator'
+require 'rack/utils'
+require 'digest/sha1'
- module Utils
- include Rack::Utils
- extend self
- end
+module Rack::Cache
- # A variety of cache storage implementations.
+ # Rack::Cache supports pluggable storage backends.
+ #
+ # == Cacheable Objects
+ #
+ # Storage providers are responsible for storing, retreiving,
+ # and purging "cacheable objects". A cacheable object is a
+ # constrained version of the response object defined by Rack:
+ #
+ # http://rack.rubyforge.org/doc/files/SPEC.html
+ #
+ # A Rack response object is a three-element array that contains
+ # the HTTP response status code, a Hash of HTTP headers, and a
+ # response body:
+ # [ status, headers, body ]
+ #
module Storage
- # Useful base class for
+ # Storage Provider interface and abstract base class.
class Provider
+ # Retrieve a cacheable object for the key provided.
def get(key)
if value = fetch(key)
value
@@ -41,9 +55,32 @@ def store(key, object)
raise NotImplemented
end
+ def flush
+ raise NotImplemented
+ end
+
+ private
+
+ # Return an idempotent version of a Rack response body. When the
+ # object provided is an Array, return the body provided. Otherwise
+ # read the body into an Array, call the close method, and return
+ # the Array.
+ def slurp(body)
+ if body.kind_of?(Array)
+ body
+ else
+ data = []
+ body.each { |part| data << part }
+ body.close if body.respond_to? :close
+ data
+ end
+ end
+
end
- # Stores cached entries in memory using a normal Hash object.
+
+ # Stores cached entries in memory using a normal Hash object. Note that
+ # entire response bodies are kept on the heap until purged or deleted.
class Memory < Provider
def initialize(hashish={})
@@ -65,12 +102,90 @@ def fetch(key)
end
def store(key, object)
- @contents[key] = object
+ status, headers, body = object
+ data = slurp(body)
+ @contents[key] = [ status, headers, data ]
end
end
+ # A simple Hash-based memory store that writes bodies to disk.
+ class DiskBackedMemory < Memory
+ include FileUtils
+
+ def initialize(storage_root="/tmp/r#{$$}")
+ @storage_root = storage_root
+ mkdir_p @storage_root
+ super()
+ end
+
+ protected
+
+ def fetch(key)
+ if object = super
+ status, headers, sha = object
+ [ status, headers, disk_read(sha) ]
+ end
+ end
+
+ def store(key, object)
+ status, headers, body = object
+ sha = disk_write(body)
+ @contents[key] = [ status, headers, sha ]
+ [ status, headers, disk_read(sha) ]
+ end
+
+ def delete(key)
+ if object = super
+ status, headers, sha = object
+ F.unlink body_path(sha)
+ end
+ end
+
+ private
+
+ F = File
+ D = Dir
+
+ def storage_path(stem)
+ F.join(@storage_root, stem)
+ end
+
+ def partition_sha(sha)
+ sha = sha.dup
+ sha[2,0] = '/'
+ sha
+ end
+
+ def body_path(sha)
+ storage_path(partition_sha(sha))
+ end
+
+ def disk_write(body)
+ # TODO can only write one body at a time
+ temp_file = storage_path('CURRENT')
+ digest = Digest::SHA1.new
+ F.open(temp_file, 'w') do |wr|
+ body.each do |part|
+ digest << part
+ wr.write(part)
+ end
+ end
+ sha = digest.hexdigest
+ path = body_path(sha)
+ mkdir_p F.dirname(path)
+ mv temp_file, path
+ sha
+ end
+
+ def disk_read(sha)
+ path = body_path(sha)
+ File.open(path, 'r')
+ end
+
+ end
+
end
end
View
5 test/cache_test.rb
@@ -19,9 +19,6 @@ def dumb_app(env)
describe 'Rack::Cache::new' do
before { @app = method(:dumb_app) }
- it 'is defined' do
- Rack::Cache.should.respond_to? :new
- end
it 'takes a backend and returns a middleware component' do
Rack::Cache.new(@app).
should.respond_to :call
@@ -30,7 +27,7 @@ def dumb_app(env)
lambda { Rack::Cache.new(@app, {}) }.
should.not.raise(ArgumentError)
end
- it 'takes a block and executes during initialization' do
+ it 'takes a block; executes it during initialization' do
state, block_scope = 'not invoked', nil
object =
Rack::Cache.new @app do
View
9 test/config_test.rb
@@ -16,16 +16,13 @@ def configured?
end
describe 'Rack::Cache::Config' do
-
before(:each) { @config = MockConfig.new }
- it 'has events and trace variables after creation' do
- @config.events.should.not.be.nil
- @config.trace.should.not.be.nil
+ it 'has events after instantiation' do
+ @config.events.should.respond_to :[]
end
-
- it 'executes event handlers' do
+ it 'defines and executes event handlers' do
executed = false
@config.on(:foo) { executed = true }
@config.perform :foo
View
17 test/context_test.rb
@@ -17,7 +17,7 @@ def cacheable_response(*args)
# response date is 5 seconds ago; makes expiration tests easier
res['Date'] = (Time.now - 5).httpdate
res['Expires'] = (Time.now + 5).httpdate
- yield req,res if block_given?
+ yield req, res if block_given?
end
end
@@ -25,7 +25,7 @@ def validatable_response(*args)
simple_resource *args do |req,res|
res['Date'] = (Time.now - 5).httpdate
res['Last-Modified'] = res['Date']
- yield req,res if block_given?
+ yield req, res if block_given?
end
end
@@ -49,7 +49,7 @@ def validatable_response(*args)
it 'passes on requests with Authorization' do
@app = cacheable_response
- get '/',
+ get '/',
'HTTP_AUTHORIZATION' => 'basic foobarbaz'
@response.should.be.ok
@context.should.a.performed :pass
@@ -184,15 +184,4 @@ def post(*args, &b)
request(:post, *args, &b)
end
-private
-
- def method_missing(method_name, *args, &b)
- if @response
- @response.send method_name, *args, &b
- else
- super
- end
- end
-
-
end
View
0  test/provider_test.rb
No changes.
View
94 test/storage_test.rb
@@ -1,53 +1,65 @@
require "#{File.dirname(__FILE__)}/spec_setup"
-describe 'Rack::Cache::Storage' do
-
- shared_context 'Provider' do
-
- before(:each) { @cache = create_provider }
- after(:each) { @cache = nil }
-
- it 'stores objects with #put' do
- @cache.put 'hello/word', "I'm here"
- end
-
- it 'returns object stored after storing with #put' do
- @cache.put('hello/world', "I'm here").should.be == "I'm here"
- end
-
- it 'returns stored objects with #get' do
- @cache.put('foo', 'bar')
- @cache.get('foo').should.be == 'bar'
- end
-
- it 'stores and returns the value yielded by the block when no object exists with #get' do
- @cache.get('foo') { 'bar' }.should.be == 'bar'
- @cache.get('foo').should.be == 'bar'
- end
+def cacheable(body=nil)
+ [ 200, {}, [body || 'Hi'] ]
+end
- it 'does not invoke block or overwrite existing objects when block provided to #get' do
- @cache.put('foo', 'bar')
- @cache.get('foo').should.be == 'bar'
- @cache.get('foo') { baz }.should.be == 'bar'
- @cache.get('foo') { fail }
- end
+class Array
+ def cache_canonical
+ status, headers, body = self
+ body = [body.read] if body.respond_to?(:read)
+ [ status, headers, body ]
end
+end
- describe 'Memory' do
- behaves_like 'Rack::Cache::Storage Provider'
+shared_context 'A Cache Storage Provider' do
+ it 'stores objects with #put' do
+ @cache.put 'hello/world', cacheable("I'm here")
+ @cache.get('hello/world').should.not.be.nil
+ end
+ it 'returns stored objects with #get' do
+ @cache.put('foo', cacheable('bar'))
+ @cache.get('foo').cache_canonical.
+ should.be == cacheable('bar')
+ end
+ it 'returns object stored after storing with #put' do
+ @cache.put('hello/world', cacheable("I'm here")).cache_canonical.
+ should.be == cacheable("I'm here")
+ end
+ it 'stores and returns the value yielded by the block when no object exists with #get' do
+ @cache.get('foo') { cacheable('bar') }.cache_canonical.
+ should.be == cacheable('bar')
+ @cache.get('foo').cache_canonical.
+ should.be == cacheable('bar')
+ end
+ it 'does not invoke block or overwrite existing objects when block provided to #get' do
+ @cache.put('foo', cacheable('bar'))
+ @cache.get('foo').cache_canonical.should.be == cacheable('bar')
+ @cache.get('foo') { baz }.cache_canonical.should.be == cacheable('bar')
+ @cache.get('foo') { fail 'should not be called' }
+ end
+end
- it 'takes a Hash to ::new and uses it' do
- Rack::Cache::Storage::Memory.new('foo' => 'bar').get('foo').
- should.be == 'bar'
- end
+describe 'Rack::Cache::Storage' do
- it 'takes no args to ::new and creates a Hash' do
- Rack::Cache::Storage::Memory.new.get('foo').should.be.nil
+ describe 'Memory' do
+ behaves_like 'A Cache Storage Provider'
+ before { @cache = Rack::Cache::Storage::Memory.new }
+ describe '::new' do
+ it 'takes a Hash and uses it' do
+ Rack::Cache::Storage::Memory.new('foo' => cacheable('bar')).get('foo').
+ should.be == cacheable('bar')
+ end
+ it 'uses its own Hash with no args' do
+ Rack::Cache::Storage::Memory.new.get('foo').
+ should.be.nil
+ end
end
+ end
- def create_provider
- Rack::Cache::Storage::Memory.new
- end
+ describe 'DiskBackedMemory' do
+ behaves_like 'A Cache Storage Provider'
+ before { @cache = Rack::Cache::Storage::DiskBackedMemory.new }
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.