Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add a fault_tolerant flag to fail-over to stale cache. #91

Open
wants to merge 43 commits into from

8 participants

@mszenher

Rack::Cache includes an option to enable fault tolerant caching, so that stale cached results can be returned if the downstream service is unavailable. Just set :fault_tolerant => true in the options hash.

All automated tests continue to pass.

@fosdev

I would bracket the end as well if you feel it is important to reference this.

I left it in by accident, it was just to help me find my new test while I was working on it.

@fosdev

Do we really want to add this dependency? Seems to me that if you are modifying errors that can be caught, it would be better to just add some kind of configuration of that, if it can be done.

Yeah, we can't assume this'll be used in a faraday context as this gem can be used for rack-middleware as well as faraday middleware.

@fosdev

Do you need to include the zero. If headers['Age'] is nil that will give a zero and in the logic before, if there is a 'Age' header, it wins regardless and presumbably the 0 corresponses to nil.

Also, you don't need max.to_i since everything will already be an integer.

The 'Age' header will generally be zero (assuming no intermediate caches), so it usually won't win. The zero is for the case where our client computer's time is not in sync to the server's, and we get a date header from the server that is in the future. It seemed like a bad idea to have a negative age.

@fosdev

Suggested: whitespace +=

@fosdev

Indentation here and below

clabrunda and others added some commits
@clabrunda clabrunda Fixed formatting. 048fa67
@clabrunda clabrunda Made fault tolerance optional and removed faraday dependency. bdf25a8
@clabrunda clabrunda Removed unused function. c8f7fc4
@clabrunda clabrunda Fixed line length. 6d9fd24
@clabrunda clabrunda Added todo comment about configurable exceptions. 1005c9a
@mszenher mszenher Merge pull request #2 from mdsol/add_fault_tolerance
Add fault tolerance
e3ed495
@clabrunda clabrunda Don't set HTTP_IF_NONE_MATCH to nil if etags missing. 1dd0422
@HonoreDB HonoreDB Merge pull request #3 from mdsol/fix_etag_bug
Don't set HTTP_IF_NONE_MATCH to nil if etags missing
c300053
Chaim Solomon Moved fault_tolerant? call into method and evaluate the middleware_op…
…tion fallback_to_cache.
ea4a4a2
Chaim Solomon Don't assume that we have middleware_options.
For Eureka-client we can assume them to be present but in other contexts they may not be around.
bd8d6e9
Chaim Solomon Monkey-Patch Rack::MockRequest to pass in the middleware options (as …
…Euresource/Eureka client does).
4d4c550
Chaim Solomon Added tests to test the per-request fault-tolerance option dc85436
Chaim Solomon Added comment for the fault_tolerant_condition 9f813ee
Chaim Solomon Addressed comments f237804
Chaim Solomon Added more explicit logging of failover 74cca24
Chaim Solomon Added retry-logic: Retry <retry> number of times, then (if fault-tole…
…rant) fail-over to stale date or just fail.
3a04036
Chaim Solomon Bumped version a3cc50a
Chaim Solomon Added retry logic to miss case (no data in cache) dec8346
Chaim Solomon Addressed comment a1eff24
Chaim Solomon Added tests for retry cases f7de10e
Chaim Solomon Addressed comment b563d4e
Chaim Solomon Addressed comment 940d4f7
Chaim Solomon Addressed comment 8f50362
Chaim Solomon Addressed comment 80de73e
Chaim Solomon Addressed comment 45cd5f0
Chaim Solomon Addressed comment 8b2e7af
Chaim Solomon Fixed wrong record 57cdc96
Chaim Solomon Added test for no-retry fail case 327e458
@clabrunda clabrunda Merge pull request #4 from mdsol/feature/per_request_fallback_to_cache
Feature/per request fallback to cache
df84971
@BPONTES BPONTES Make sure middleware_options is not set to nil 3b6d767
@BPONTES BPONTES Make sure middleware_options is not set to nil, in a rails fashion 26f65e7
@mszenher mszenher update comments 05bef08
@mszenher mszenher add spec c64187a
@fosdev fosdev Merge pull request #5 from mdsol/feature/nil_middleware_options
Feature/nil middleware options
5fc9137
@fosdev fosdev Merge pull request #6 from mdsol/develop
Develop
d7fd826
lib/rack/cache/context.rb
@@ -15,6 +15,10 @@ class Context
# The Rack application object immediately downstream.
attr_reader :backend
+ # Set of exceptions that indicate a network connection failure.
+ # TODO: Make these configurable, to work in a non-faraday environment.
+ EXCEPTION_CLASSES = Set.new %w(Timeout::Error Faraday::Error::ConnectionFailed Faraday::Error::TimeoutError)
@rtomayko Owner

Hmm, maybe just move these to a class instance variable at Rack::Cache.network_failure_exceptions for now so it's a bit more configurable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rack/cache/context.rb
((9 lines not shown))
+ if retry_counter < retries
+ retry_counter += 1
+ record "Retrying #{retry_counter} of #{retries} times due to #{e.class.name}: #{e.to_s}"
+ retry
+ else
+ record "Failed retry after #{retries} retries due to #{e.class.name}: #{e.to_s}"
+ raise
+ end
+ end
+ rescue lambda { |error| fault_tolerant_condition && EXCEPTION_CLASSES.include?(error.class.name) } => e
+ record :connnection_failed
+ age = entry.age.to_s
+ entry.headers['Age'] = age
+ record "Fail-over to stale cache data with age #{age} due to #{e.class.name}: #{e.to_s}"
+ entry
+ end
@rtomayko Owner

Hmm I guess it'd be tricky with the retry var but it'd be great to pull this out into a separate method. It'd be a bit more descriptive, could possibly get rid of a begin/end, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@rtomayko
Owner

This is really cool. Thanks for the patch!

See what you think about the comments I left and I'll circle back here to see about merging.

@fosdev

@rtomayko Comments addressed. Feel free to re-review.

@swrobel swrobel referenced this pull request in redis-store/redis-store
Open

Redis Failure #175

@mszenher

@rtomayko Any chance this can be merged? Thanks.

@roberto

Is there any progress here?

@tulios

It would be nice to have this merged, any progress?

@mszenher

I think this is just awaiting @rtomayko 's final approval and merge. @rtomayko, is there anything more you want us contributors to do in this PR or is it ready to merge? Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 18, 2013
  1. @clabrunda
  2. @clabrunda

    Fixed formatting.

    clabrunda authored
Commits on Apr 22, 2013
  1. @clabrunda
Commits on Apr 23, 2013
  1. @clabrunda

    Removed unused function.

    clabrunda authored
Commits on Apr 25, 2013
  1. @clabrunda

    Fixed line length.

    clabrunda authored
  2. @clabrunda
Commits on Apr 30, 2013
  1. @mszenher

    Merge pull request #2 from mdsol/add_fault_tolerance

    mszenher authored
    Add fault tolerance
Commits on May 10, 2013
  1. @clabrunda
Commits on May 15, 2013
  1. @HonoreDB

    Merge pull request #3 from mdsol/fix_etag_bug

    HonoreDB authored
    Don't set HTTP_IF_NONE_MATCH to nil if etags missing
Commits on Jun 11, 2013
  1. Moved fault_tolerant? call into method and evaluate the middleware_op…

    Chaim Solomon authored
    …tion fallback_to_cache.
  2. Don't assume that we have middleware_options.

    Chaim Solomon authored
    For Eureka-client we can assume them to be present but in other contexts they may not be around.
Commits on Jun 12, 2013
  1. Monkey-Patch Rack::MockRequest to pass in the middleware options (as …

    Chaim Solomon authored
    …Euresource/Eureka client does).
  2. Added comment for the fault_tolerant_condition

    Chaim Solomon authored
  3. Addressed comments

    Chaim Solomon authored
  4. Added more explicit logging of failover

    Chaim Solomon authored
Commits on Jun 17, 2013
  1. Added retry-logic: Retry <retry> number of times, then (if fault-tole…

    Chaim Solomon authored
    …rant) fail-over to stale date or just fail.
Commits on Jun 18, 2013
  1. Bumped version

    Chaim Solomon authored
  2. Added retry logic to miss case (no data in cache)

    Chaim Solomon authored
  3. Addressed comment

    Chaim Solomon authored
  4. Added tests for retry cases

    Chaim Solomon authored
  5. Addressed comment

    Chaim Solomon authored
  6. Addressed comment

    Chaim Solomon authored
  7. Addressed comment

    Chaim Solomon authored
  8. Addressed comment

    Chaim Solomon authored
  9. Addressed comment

    Chaim Solomon authored
  10. Addressed comment

    Chaim Solomon authored
  11. Fixed wrong record

    Chaim Solomon authored
  12. Added test for no-retry fail case

    Chaim Solomon authored
  13. @clabrunda

    Merge pull request #4 from mdsol/feature/per_request_fallback_to_cache

    clabrunda authored
    Feature/per request fallback to cache
Commits on Sep 17, 2013
  1. @BPONTES
  2. @BPONTES
Commits on Sep 30, 2013
  1. @mszenher

    update comments

    mszenher authored
  2. @mszenher

    add spec

    mszenher authored
  3. @fosdev

    Merge pull request #5 from mdsol/feature/nil_middleware_options

    fosdev authored
    Feature/nil middleware options
Commits on Oct 2, 2013
  1. @fosdev

    Merge pull request #6 from mdsol/develop

    fosdev authored
    Develop
Commits on Oct 11, 2013
  1. @fosdev
  2. @fosdev

    Add comment to method.

    fosdev authored
  3. @clabrunda

    Merge pull request #7 from mdsol/feature/updates_per_rtomayko

    clabrunda authored
    added self documenting methods and a little DRYness
  4. @fosdev

    Removed missed constant

    fosdev authored
  5. @clabrunda

    Merge pull request #8 from mdsol/feature/updates_per_rtomayko

    clabrunda authored
    Removed missed constant
Commits on Oct 24, 2013
  1. @fosdev
Commits on Oct 30, 2013
  1. @clabrunda

    Merge pull request #9 from mdsol/feature/fix_typo

    clabrunda authored
    Feature/fix typo
This page is out of date. Refresh to see the latest.
View
1  Gemfile
@@ -1,2 +1,3 @@
source :rubygems
+gem 'rake', '~> 10.0'
gemspec
View
4 README
@@ -24,6 +24,10 @@ caching solution for small to medium sized deployments. More sophisticated /
high-performance caching systems (e.g., Varnish, Squid, httpd/mod-cache) may be
more appropriate for large deployments with significant throughput requirements.
+Rack::Cache includes an option to enable fault tolerant caching, so that stale
+cached results can be returned if the downstream service is unavailable. Just
+set `:fault_tolerant => true` in the options hash.
+
Installation
------------
View
89 lib/rack/cache/context.rb
@@ -6,6 +6,15 @@
module Rack::Cache
# Implements Rack's middleware interface and provides the context for all
# cache logic, including the core logic engine.
+
+ # The list of exceptions indicating a network failure.
+ def self.network_failure_exceptions
+ # TODO: Make these configurable, to work in a non-faraday environment.
+ @network_failure_exceptions ||= Set.new(
+ %w(Timeout::Error Faraday::Error::ConnectionFailed Faraday::Error::TimeoutError)
+ )
+ end
+
class Context
include Rack::Cache::Options
@@ -159,7 +168,9 @@ def invalidate
# found and is fresh, use it as the response without forwarding any
# request to the backend. When a matching cache entry is found but is
# stale, attempt to #validate the entry with the backend using conditional
- # GET. When no matching cache entry is found, trigger #miss processing.
+ # GET. If validation fails due to a timeout or connection error, serve the
+ # stale cache entry anyway. When no matching cache entry is found, trigger
+ # miss processing.
def lookup
if @request.no_cache? && allow_reload?
record :reload
@@ -178,14 +189,45 @@ def lookup
entry
else
record :stale
- validate(entry)
+ validate_with_retries_and_stale_cache_failover(entry)
end
else
record :miss
- fetch
+ fetch_with_retries
end
end
end
+
+ # Returns stale cache on timeout or connection error.
+ def validate_with_retries_and_stale_cache_failover(entry)
+ begin
+ send_with_retries(:validate, entry)
+ rescue lambda { |error| fault_tolerant_condition? && network_failure_exception?(error) } => e
+ record :connnection_failed
+ age = entry.age.to_s
+ entry.headers['Age'] = age
+ record "Fail-over to stale cache data with age #{age} due to #{e.class.name}: #{e.to_s}"
+ entry
+ end
+ end
+
+ # Calls fetch wrapped with retries.
+ def fetch_with_retries
+ send_with_retries(:fetch)
+ end
+
+ #This method is used in the lambda of lookup (a few lines up) to test if in an error case the fallback to stale
+ #data should be performed.
+ #If the per-request parameter :fallback_to_cache is in the middleware options then it will be used to decide.
+ #If it is not present, then the global setting will be honored.
+ #Setting the per-request option to false overrides the global settings!
+ def fault_tolerant_condition?
+ if @request.env.include?(:middleware_options) && @request.env[:middleware_options].include?(:fallback_to_cache)
+ @request.env[:middleware_options][:fallback_to_cache]
+ else
+ fault_tolerant?
+ end
+ end
# Validate that the cache entry is fresh. The original request is used
# as a template for a conditional GET request with the backend.
@@ -194,7 +236,7 @@ def validate(entry)
@env['REQUEST_METHOD'] = 'GET'
# add our cached last-modified validator to the environment
- @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
+ @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified if entry.last_modified
# Add our cached etag validator to the environment.
# We keep the etags from the client to handle the case when the client
@@ -202,8 +244,7 @@ def validate(entry)
cached_etags = entry.etag.to_s.split(/\s*,\s*/)
request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
etags = (cached_etags + request_etags).uniq
- @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')
-
+ @env['HTTP_IF_NONE_MATCH'] = etags.join(', ') unless etags.empty?
response = forward
if response.status == 304
@@ -295,5 +336,41 @@ def log(level, message)
@env['rack.errors'].write(message)
end
end
+
+ private
+
+ # How many times has the consumer requested retries.
+ def requested_retries
+ retries_defined? ? @request.env[:middleware_options][:retries].to_i : 0
+ end
+
+ # Whether the consumer asked us to retry.
+ def retries_defined?
+ @request.env[:middleware_options] && @request.env[:middleware_options][:retries]
+ end
+
+ # Whether the error is a known network failure exception or not.
+ def network_failure_exception?(error)
+ Rack::Cache.network_failure_exceptions.include?(error.class.name)
+ end
+
+ # Sends a method and wraps it with a retry loop if retries are requested
+ def send_with_retries(method, *args)
+ retries = requested_retries
+ retry_counter = 0
+
+ begin
+ send(method, *args)
+ rescue lambda { |error| (retries > 0) && network_failure_exception?(error) } => e
+ if retry_counter < retries
+ retry_counter += 1
+ record "Retrying #{retry_counter} of #{retries} times due to #{e.class.name}: #{e.to_s}"
+ retry
+ else
+ record "Failed retry after #{retries} retries due to #{e.class.name}: #{e.to_s}"
+ raise
+ end
+ end
+ end
end
end
View
6 lib/rack/cache/options.rb
@@ -107,6 +107,11 @@ def option_name(key)
# be used.
option_accessor :use_native_ttl
+ # Specifies whether to serve a request from a stale cache entry if
+ # the attempt to revalidate that entry returns a connection
+ # failure or times out.
+ option_accessor :fault_tolerant
+
# The underlying options Hash. During initialization (or outside of a
# request), this is a default values Hash. During a request, this is the
# Rack environment Hash. The default values Hash is merged in underneath
@@ -149,6 +154,7 @@ def initialize_options(options={})
'rack-cache.allow_reload' => false,
'rack-cache.allow_revalidate' => false,
'rack-cache.use_native_ttl' => false,
+ 'rack-cache.fault_tolerant' => false,
}
self.options = options
end
View
2  lib/rack/cache/response.rb
@@ -146,7 +146,7 @@ def date
# The age of the response.
def age
- (headers['Age'] || [(now - date).to_i, 0].max).to_i
+ [headers['Age'].to_i, (now - date).to_i].max
end
# The number of seconds after the time specified in the response's Date
View
5 rack-cache.gemspec
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.name = 'rack-cache'
- s.version = '1.2'
- s.date = '2012-03-05'
+ s.version = '1.3'
+ s.date = '2013-06-18'
s.summary = "HTTP Caching for Rack"
s.description = "Rack::Cache is suitable as a quick drop-in component to enable HTTP caching for Rack-based applications that produce freshness (Expires, Cache-Control) and/or validation (Last-Modified, ETag) information."
@@ -66,6 +66,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'bacon'
s.add_development_dependency 'memcached'
s.add_development_dependency 'dalli'
+ s.add_development_dependency 'pry'
s.has_rdoc = true
s.homepage = "http://rtomayko.github.com/rack-cache/"
View
284 test/context_test.rb
@@ -264,7 +264,7 @@
'when allow_reload is set true' do
count = 0
respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
- count+= 1
+ count += 1
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
end
@@ -290,7 +290,7 @@
it 'does not reload responses when allow_reload is set false (default)' do
count = 0
respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
- count+= 1
+ count += 1
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
end
@@ -323,7 +323,7 @@
'when allow_revalidate option is set true' do
count = 0
respond_with do |req,res|
- count+= 1
+ count += 1
res['Cache-Control'] = 'max-age=10000'
res['ETag'] = count.to_s
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
@@ -349,10 +349,284 @@
cache.trace.should.include :store
end
+ it 'returns a stale cache entry when max-age request directive is exceeded ' +
+ 'when allow_revalidate and fault_tolerant options are set to true and ' +
+ 'the remote server returns a connection error' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count == 2
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ get '/'
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :store
+
+ get '/',
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => true,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0'
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :stale
+ cache.trace.should.include :connnection_failed
+
+ # Once the server comes back, the request should be revalidated.
+ get '/',
+ 'rack-cache.allow_revalidate' => true,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0'
+ response.should.be.ok
+ response.body.should.equal 'Goodbye World'
+ cache.trace.should.include :stale
+ cache.trace.should.include :invalid
+ cache.trace.should.include :store
+ end
+
+ it 'returns a stale cache entry when max-age request directive is exceeded ' +
+ 'when allow_revalidate and per-request fault_tolerant options are set to true and ' +
+ 'the remote server returns a connection error' do
+ count = 0
+ respond_with do |req, res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count == 2
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ get '/'
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :store
+
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {fallback_to_cache: true}
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :stale
+ cache.trace.should.include :connnection_failed
+
+ # Once the server comes back, the request should be revalidated.
+ get '/',
+ 'rack-cache.allow_revalidate' => true,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0'
+ response.should.be.ok
+ response.body.should.equal 'Goodbye World'
+ cache.trace.should.include :stale
+ cache.trace.should.include :invalid
+ cache.trace.should.include :store
+ end
+
+ it 'retries on connection failures as configured in the middleware options and succeeds after 2 retries' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count < 3
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 3) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {retries: 2}
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :miss
+ cache.trace.should.include "Retrying 1 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include "Retrying 2 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include :store
+ end
+
+ it 'retries on connection failures as configured in the middleware options and fails after 2 retries in cache miss case' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count < 4
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 3) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ lambda { Rack::Cache.new(@app, {})
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {retries: 2}
+ }.should.raise(Timeout::Error)
+ cache.trace.should.include :miss
+ cache.trace.should.include "Retrying 1 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include "Retrying 2 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include "Failed retry after 2 retries due to Timeout::Error: Connection failed"
+ end
+
+ it 'does not retry on connection failures if retries is not configured in the middleware options and fails in cache miss case' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count < 4
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 3) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ lambda { Rack::Cache.new(@app, {})
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {retries: 0}
+ }.should.raise(Timeout::Error)
+ cache.trace.should.include :miss
+ end
+
+ it 'does not retry on connection failures if no middleware options are configured and fails in cache miss case' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count < 4
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 3) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ lambda { Rack::Cache.new(@app, {})
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0'
+ }.should.raise(Timeout::Error)
+ cache.trace.should.include :miss
+ end
+
+ it 'retries on connection failures as configured in the middleware options and fails after 3 retries in hit case' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if (2..6).include? count
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {fallback_to_cache: true}
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :miss
+ cache.trace.should.include :store
+
+ lambda { Rack::Cache.new(@app, {})
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {retries: 2}
+ }.should.raise(Timeout::Error)
+ cache.trace.should.include :stale
+ cache.trace.should.include "Retrying 1 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include "Retrying 2 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include "Failed retry after 2 retries due to Timeout::Error: Connection failed"
+ end
+
+ it 'retries on connection failures as configured in the middleware options and reverts to stale data after 3 retries in hit case' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if (2..6).include? count
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => false,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {fallback_to_cache: true}
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :miss
+ cache.trace.should.include :store
+
+ get '/', # This tests if the per-request setting of the fallback to cache works
+ 'rack-cache.allow_revalidate' => true,
+ 'rack-cache.fault_tolerant' => true,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ :middleware_options => {retries: 2}
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :stale
+ cache.trace.should.include "Retrying 1 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include "Retrying 2 of 2 times due to Timeout::Error: Connection failed"
+ cache.trace.should.include "Failed retry after 2 retries due to Timeout::Error: Connection failed"
+ cache.trace.should.include :connnection_failed
+ cache.trace.should.include "Fail-over to stale cache data with age 0 due to Timeout::Error: Connection failed"
+ end
+
+ it 'allows an exception to be raised when a connection error occurs ' +
+ 'while revalidating a cached entry if fault_tolerant is set to false (the default)' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count == 2
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ get '/'
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :store
+
+ lambda { get '/',
+ 'rack-cache.allow_revalidate' => true,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0' }.should.raise(Timeout::Error)
+ cache.trace.should.include :stale
+ end
+
+ it 'allows an exception to be raised when a connection error occurs ' +
+ 'while revalidating a cached entry if fault_tolerant is set to true but the per-request is false' do
+ count = 0
+ respond_with do |req,res|
+ count += 1
+ raise Timeout::Error, 'Connection failed' if count == 2
+ res['Cache-Control'] = 'max-age=10000'
+ res['ETag'] = count.to_s
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
+ end
+
+ get '/'
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ cache.trace.should.include :store
+
+ lambda { get '/',
+ 'rack-cache.allow_revalidate' => true,
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
+ 'rack-cache.fault_tolerant' => true,
+ :middleware_options => {fallback_to_cache: false} }.should.raise(Timeout::Error)
+ cache.trace.should.include :stale
+ end
+
it 'does not revalidate fresh cache entry when enable_revalidate option is set false (default)' do
count = 0
respond_with do |req,res|
- count+= 1
+ count += 1
res['Cache-Control'] = 'max-age=10000'
res['ETag'] = count.to_s
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
@@ -715,7 +989,7 @@
count = 0
respond_with do |req,res|
res['Last-Modified'] = timestamp
- case (count+=1)
+ case (count +=1)
when 1 ; res.body = ['first response']
when 2 ; res.body = ['second response']
when 3
View
14 test/spec_setup.rb
@@ -80,6 +80,20 @@ def need_java(forwhat)
require 'rack/cache'
+module Rack
+ class MockRequest
+ class << self
+ alias_method :new_env_for, :env_for
+ end
+ def self.env_for(uri="", opts={})
+ middleware_opts = opts.delete(:middleware_options)
+ ret_env = self.new_env_for(uri, opts)
+ ret_env[:middleware_options] = middleware_opts unless middleware_opts.nil?
+ ret_env
+ end
+ end
+end
+
# Methods for constructing downstream applications / response
# generators.
module CacheContextHelpers
Something went wrong with that request. Please try again.