Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0b3add8
Added fault tolerant caching and fixed a couple bugs.
clabrunda Apr 18, 2013
048fa67
Fixed formatting.
clabrunda Apr 18, 2013
bdf25a8
Made fault tolerance optional and removed faraday dependency.
clabrunda Apr 22, 2013
c8f7fc4
Removed unused function.
clabrunda Apr 23, 2013
6d9fd24
Fixed line length.
clabrunda Apr 25, 2013
1005c9a
Added todo comment about configurable exceptions.
clabrunda Apr 25, 2013
e3ed495
Merge pull request #2 from mdsol/add_fault_tolerance
mszenher Apr 30, 2013
1dd0422
Don't set HTTP_IF_NONE_MATCH to nil if etags missing.
clabrunda May 10, 2013
c300053
Merge pull request #3 from mdsol/fix_etag_bug
HonoreDB May 15, 2013
ea4a4a2
Moved fault_tolerant? call into method and evaluate the middleware_op…
Jun 11, 2013
bd8d6e9
Don't assume that we have middleware_options.
Jun 11, 2013
4d4c550
Monkey-Patch Rack::MockRequest to pass in the middleware options (as …
Jun 12, 2013
dc85436
Added tests to test the per-request fault-tolerance option
Jun 12, 2013
9f813ee
Added comment for the fault_tolerant_condition
Jun 12, 2013
f237804
Addressed comments
Jun 12, 2013
74cca24
Added more explicit logging of failover
Jun 12, 2013
3a04036
Added retry-logic: Retry <retry> number of times, then (if fault-tole…
Jun 17, 2013
a3cc50a
Bumped version
Jun 18, 2013
dec8346
Added retry logic to miss case (no data in cache)
Jun 18, 2013
a1eff24
Addressed comment
Jun 18, 2013
f7de10e
Added tests for retry cases
Jun 18, 2013
b563d4e
Addressed comment
Jun 18, 2013
940d4f7
Addressed comment
Jun 18, 2013
8f50362
Addressed comment
Jun 18, 2013
80de73e
Addressed comment
Jun 18, 2013
45cd5f0
Addressed comment
Jun 18, 2013
8b2e7af
Addressed comment
Jun 18, 2013
57cdc96
Fixed wrong record
Jun 18, 2013
327e458
Added test for no-retry fail case
Jun 18, 2013
df84971
Merge pull request #4 from mdsol/feature/per_request_fallback_to_cache
clabrunda Jun 18, 2013
3b6d767
Make sure middleware_options is not set to nil
Sep 17, 2013
26f65e7
Make sure middleware_options is not set to nil, in a rails fashion
Sep 17, 2013
05bef08
update comments
mszenher Sep 30, 2013
c64187a
add spec
mszenher Sep 30, 2013
5fc9137
Merge pull request #5 from mdsol/feature/nil_middleware_options
fosdev Sep 30, 2013
d7fd826
Merge pull request #6 from mdsol/develop
fosdev Oct 2, 2013
1cd21d3
added self documenting methods and a little DRYness
fosdev Oct 11, 2013
41596f6
Add comment to method.
fosdev Oct 11, 2013
5ef9309
Merge pull request #7 from mdsol/feature/updates_per_rtomayko
clabrunda Oct 11, 2013
ed54aa8
Removed missed constant
fosdev Oct 11, 2013
796b84c
Merge pull request #8 from mdsol/feature/updates_per_rtomayko
clabrunda Oct 11, 2013
78005c2
fixed typo and updated method calls
fosdev Oct 24, 2013
9bda595
Merge pull request #9 from mdsol/feature/fix_typo
clabrunda Oct 30, 2013
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
source :rubygems
gem 'rake', '~> 10.0'
gemspec
4 changes: 4 additions & 0 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------

Expand Down
89 changes: 83 additions & 6 deletions lib/rack/cache/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -194,16 +236,15 @@ 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
# has a different private valid entry which is not cached here.
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
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions lib/rack/cache/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/cache/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions rack-cache.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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/"
Expand Down
Loading