Skip to content

Commit

Permalink
Invalidate instead of purge on non-GET/POST requests
Browse files Browse the repository at this point in the history
Sets the Age header to the max_age + 1 before storing the
entry, causing it to be invalid the next time its retrieved from
cache. The Age header is no longer written when storing fresh/valid
entries.
  • Loading branch information
Daniel Mendler authored and rtomayko committed Mar 7, 2009
1 parent 5a48ad5 commit e4a7b58
Show file tree
Hide file tree
Showing 7 changed files with 33 additions and 18 deletions.
3 changes: 2 additions & 1 deletion CHANGES
Expand Up @@ -2,7 +2,8 @@

* Invalidate cache entries that match the request URL on non-GET/HEAD
requests. i.e., POST, PUT, DELETE cause matching cache entries to
be purged.
be invalidated. The cache entry is validated with the backend using
a conditional GET the next time it's requested.

* Implement "Cache-Control: max-age=N" request directive by forcing
validation when the max-age provided exceeds the age of the cache
Expand Down
2 changes: 0 additions & 2 deletions lib/rack/cache/context.rb
Expand Up @@ -135,7 +135,6 @@ def dispatch
# tidy up response a bit
response.not_modified! if not_modified?(response)
response.body = [] if @original_request.head?
response.headers.delete 'X-Status'
response.to_a
end

Expand Down Expand Up @@ -195,7 +194,6 @@ def validate(entry)
if backend_response.status == 304
record :valid
entry = entry.dup
entry.headers.delete('Age')
entry.headers.delete('Date')
%w[Date Expires Cache-Control Etag Last-Modified].each do |name|
next unless value = backend_response.headers[name]
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/cache/headers.rb
Expand Up @@ -200,7 +200,7 @@ def date

# The age of the response.
def age
[(now - date).to_i, 0].max
(headers['Age'] || [(now - date).to_i, 0].max).to_i
end

# The number of seconds after the time specified in the response's Date
Expand Down
21 changes: 13 additions & 8 deletions lib/rack/cache/metastore.rb
Expand Up @@ -38,7 +38,8 @@ def lookup(request, entity_store)

req, res = match
if body = entity_store.open(res['X-Content-Digest'])
response = Rack::Cache::Response.new(res['X-Status'].to_i, res, body)
status = res.delete('X-Status').to_i
response = Rack::Cache::Response.new(status, res, body)
response.activate!
response
else
Expand All @@ -57,7 +58,6 @@ def store(request, response, entity_store)

# write the response body to the entity store if this is the
# original response.
response['X-Status'] = response.status.to_s
if response['X-Content-Digest'].nil?
digest, size = entity_store.write(response.body)
response['X-Content-Digest'] = digest
Expand All @@ -74,7 +74,11 @@ def store(request, response, entity_store)
(vary == res['Vary']) &&
requests_match?(vary, env, stored_env)
end
entries.unshift [stored_env, {}.update(response.headers)]

headers = {'X-Status' => response.status.to_s}.update(response.headers)
headers.delete 'Age'

entries.unshift [stored_env, headers]
write key, entries
key
end
Expand All @@ -86,14 +90,15 @@ def cache_key(request)
end

# Invalidate all cache entries that match the request.
#
# TODO: This should not purge the entries but rather mark them as
# stale so that they're revalidated on the next request.
def invalidate(request, entity_store)
key = cache_key(request)
entries = read(key)
entries.each { |req,res| entity_store.purge(res['X-Content-Digest']) }
purge(key)
entries = entries.map do |req, res|
res = Rack::Cache::Response.new(0, res, nil)
res.headers['Age'] = res.max_age + 1
[req, res.headers]
end
write key, entries
end

private
Expand Down
3 changes: 1 addition & 2 deletions lib/rack/cache/response.rb
Expand Up @@ -36,7 +36,6 @@ def initialize(status, headers, body)
@status = status
@headers = Rack::Utils::HeaderHash.new(headers)
@body = body
@now = Time.now
@headers['Date'] ||= now.httpdate
end

Expand All @@ -57,7 +56,7 @@ def []=(header_name, header_value)

# Called immediately after an object is loaded from the cache.
def activate!
headers['Age'] = age.to_i.to_s
headers['Age'] = age.to_s
end

# Return the status, headers, and body in a three-tuple.
Expand Down
14 changes: 12 additions & 2 deletions test/context_test.rb
Expand Up @@ -23,6 +23,7 @@
app.should.be.called
response.should.be.ok
cache.trace.should.include :invalidate
cache.trace.should.include :pass
end
end

Expand Down Expand Up @@ -555,7 +556,7 @@
response['Content-Length'].should.equal 'Hello World'.length.to_s
end

it 'purges cached responses on POST' do
it 'invalidates cached responses on POST' do
respond_with do |req,res|
if req.request_method == 'GET'
res.status = 200
Expand All @@ -577,20 +578,29 @@
cache.trace.should.include :miss
cache.trace.should.include :store

# make sure it is valid
get '/'
app.should.not.called
response.should.be.ok
response.body.should.equal 'Hello World'
cache.trace.should.include :fresh

# now POST to same URL
post '/'
app.should.be.called
response.should.be.redirect
response['Location'].should.equal '/'
cache.trace.should.include :invalidate
cache.trace.should.include :pass
response.body.should.equal ''

# now make sure it was actually invalidated
get '/'
app.should.be.called
response.should.be.ok
response.body.should.equal 'Hello World'
cache.trace.should.include :miss
cache.trace.should.include :stale
cache.trace.should.include :invalid
cache.trace.should.include :store
end

Expand Down
6 changes: 4 additions & 2 deletions test/metastore_test.rb
Expand Up @@ -143,10 +143,12 @@ def self.call(request); request.path_info.reverse end
body.should.equal 'test'
end

it 'purges meta and entity store entries with #invalidate' do
it 'invalidates meta and entity store entries with #invalidate' do
store_simple_entry
@store.invalidate(@request, @entity_store)
response = @store.lookup(@request, @entity_store).should.be.nil
response = @store.lookup(@request, @entity_store)
response.should.be.kind_of Rack::Cache::Response
response.should.be.stale
end

it 'succeeds quietly when #invalidate called with no matching entries' do
Expand Down

0 comments on commit e4a7b58

Please sign in to comment.