Permalink
Browse files

add DiskBackedMemory store; other small tweaks and cleanup

  • Loading branch information...
1 parent 180c835 commit 825fb93e0351c7ee67727bfcf3851c2c873027c2 @rtomayko committed Jul 23, 2008
View
@@ -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 }
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,15 +17,15 @@ 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
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
No changes.
Oops, something went wrong.

0 comments on commit 825fb93

Please sign in to comment.