-
Notifications
You must be signed in to change notification settings - Fork 321
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
Caching #177
Closed
Closed
Caching #177
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
7f749a9
Remove obsolete BodyDelegator
ixti 0900813
Bump up YARDStick threshold
ixti 0b4c444
HTTP Caching
Asmod4n 4310b9e
Typo
Asmod4n 2533e01
Small fixups
Asmod4n e7f753b
Fix a couple of specs and remove some rubocop warnings
pezra c4f1e45
basic specs for caching
pezra 75d8068
extract some caching related helper objects and write specs
pezra b3499bd
improve cache options
pezra 153fe3b
remove dead code
pezra 69a343d
remove unused argument to make rubocop happy
pezra 40fb8b6
remove debug puts
pezra bbfd8c3
remove many rubocop issues
pezra 2a57ce3
swap `find` for `detect` to appease rubocop
pezra 31a60f6
improve factoring of cache perform method
pezra 08df766
further factoring improvements to cache perform
pezra f2346bd
Convert docs to yard
pezra 4c10ea8
blank lines break builds, apparently
pezra bcf947e
Enable align parameters cop back again
ixti 69dfa0a
Rename *WithCacheBehavior to *::Cached
ixti 156ce9d
Refacor CacheControl helper. Renamed it to Cache::Headers
ixti 0adc2a1
reenabled parameter alignment cop
pezra cd49187
Merge remote-tracking branch 'refs/remotes/upstream/caching' into cac…
pezra d22eaa5
remove a trailing line comment that made yard fail on ruby 2.2.0
pezra a33190f
rename HTTP::{Request,Response}::Cached to Caching
pezra 71bc884
remove some unused attributes from Response
pezra 5814dbb
Use rack-cache's storage implementations rather than rolling our own
pezra 429a5a0
handle the rack-cache's result when using file stores
pezra 79cdbb7
verify that cache's file store works
pezra 7153878
allow both original requester and cache store to read response body
pezra cac374d
remove a layer of indirection (and code)
pezra 042d904
fix a few minor stylistic thing for rubocop
pezra 9956494
Merge remote-tracking branch 'refs/remotes/upstream/master' into caching
pezra 182d5db
delay loading rack-cache until caching is requested
pezra 2d1f847
reduce length of options to appease rubocop
pezra 833deae
removed unused dependency
pezra 88dc181
move require to top of file
pezra 48f3e5c
simplify spec
pezra eeeb2c8
improved caching response spec
pezra 86741e2
bury the lede in some specs
pezra c234d49
remove unnecessary/incorrect date addition code in request
pezra File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
|
||
POST /1416179329 HTTP/1.1 | ||
Host: localhost:8080 | ||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:32.0) Gecko/20100101 Firefox/32.0 Iceweasel/32.0 | ||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 | ||
Accept-Language: en-US,en;q=0.5 | ||
Accept-Encoding: gzip, deflate | ||
DNT: 1 | ||
Referer: http://localhost:8080/1416179301 | ||
Cookie: __utma=111872281.776418867.1390272758.1415735077.1415742340.6; __utmz=111872281.1390272758.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _jsuid=2367279670; sid=1bad10a0201c253cfaf0bfebe50405fef82b2267; _vidiq.sess=BAh7CUkiDHVzZXJfaWQGOgZFVCIdNTQ1YThlZjg5N2YwMTQwNzk1MDAwMDAxSSIPY3JlYXRlZF9hdAY7AFRsKwcjj1pUSSIPc2Vzc2lvbl9pZAY7AFRJIiU3YTE4NGQ0YTc1ZTI0YzEwOGFhNjZmYzNkOTQ3NjkzNAY7AFRJIhFsYXN0X3JlcXVlc3QGOwBUbCsHspNaVA%3D%3D--2d06b2c91f7eddd469d2781ab14a57759dc44543; locale=en-US; _vidiq.auth=1; utag_main=v_id:0149448f5106001d3fd3082bf59d0405300160090086e$_sn:1$_ss:0$_pn:5%3Bexp-session$_st:1414196750542$ses_id:1414194483462%3Bexp-session; s_fid=7486C318933E2879-25F3BFA3ED8EC585; s_nr=1414194950805-New | ||
Connection: keep-alive | ||
Content-Type: multipart/form-data; boundary=---------------------------38016663816079156891839870716 | ||
Content-Length: 1360 | ||
|
||
-----------------------------38016663816079156891839870716 | ||
Content-Disposition: form-data; name="t1" | ||
|
||
test_1 | ||
-----------------------------38016663816079156891839870716 | ||
Content-Disposition: form-data; name="t2" | ||
|
||
test_2 | ||
-----------------------------38016663816079156891839870716 | ||
Content-Disposition: form-data; name="t2" | ||
|
||
test_3 | ||
-----------------------------38016663816079156891839870716 | ||
Content-Disposition: form-data; name="f1"; filename="index.slim" | ||
Content-Type: application/octet-stream | ||
|
||
doctype html | ||
html | ||
body | ||
form action="http://localhost" method="POST" | ||
|
||
input type="text" name="i1" | ||
input type="text" name="i2" | ||
input type="file" name="f1" | ||
textarea name="t1" | ||
|
||
input type="submit" | ||
|
||
-----------------------------38016663816079156891839870716 | ||
Content-Disposition: form-data; name="f2"; filename="index.html" | ||
Content-Type: text/html | ||
|
||
<!DOCTYPE html><html><body><form action="http://localhost" method="POST"> | ||
<input name="i1" type="text" /><input name="i2" type="text" /><input name="f1" type="file" /><textarea name="t1"></textarea><input type="submit" /></form></body></html> | ||
|
||
-----------------------------38016663816079156891839870716 | ||
Content-Disposition: form-data; name="f2"; filename="" | ||
Content-Type: application/octet-stream | ||
|
||
|
||
-----------------------------38016663816079156891839870716-- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ Gem::Specification.new do |gem| | |
|
||
gem.add_runtime_dependency "http_parser.rb", "~> 0.6.0" | ||
gem.add_runtime_dependency "http-form_data", "~> 1.0.0" | ||
gem.add_runtime_dependency "rack-cache", "~> 1.2" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes rack a hard dependency of http.rb which I don't think I really like. Perhaps you could make |
||
|
||
gem.add_development_dependency "bundler", "~> 1.0" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
require "time" | ||
require "rack-cache" | ||
|
||
module HTTP | ||
class Cache | ||
# NoOp logger. | ||
class NullLogger | ||
def error(_msg = nil) | ||
end | ||
|
||
def debug(_msg = nil) | ||
end | ||
|
||
def info(_msg = nil) | ||
end | ||
|
||
def warn(_msg = nil) | ||
end | ||
end | ||
|
||
# @return [Response] a cached response that is valid for the request or | ||
# the result of executing the provided block | ||
# | ||
# @yield [request, options] on cache miss so that an actual | ||
# request can be made | ||
def perform(request, options, &request_performer) | ||
req = request.caching | ||
|
||
invalidate_cache(req) if req.invalidates_cache? | ||
|
||
get_response(req, options, request_performer) | ||
end | ||
|
||
protected | ||
|
||
# @return [Response] the response to the request, either from the | ||
# cache or by actually making the request | ||
def get_response(req, options, request_performer) | ||
cached_resp = cache_lookup(req) | ||
return cached_resp if cached_resp && !cached_resp.stale? | ||
|
||
# cache miss | ||
logger.debug { "Cache miss for <#{req.uri}>, making request" } | ||
actual_req = if cached_resp | ||
req.conditional_on_changes_to(cached_resp) | ||
else | ||
req | ||
end | ||
actual_resp = make_request(actual_req, options, request_performer) | ||
|
||
handle_response(cached_resp, actual_resp, req) | ||
end | ||
|
||
# @returns [Response] the most useful of the responses after | ||
# updating the cache as appropriate | ||
def handle_response(cached_resp, actual_resp, req) | ||
if actual_resp.status.not_modified? && cached_resp | ||
logger.debug { "<#{req.uri}> not modified, using cached version." } | ||
cached_resp.validated!(actual_resp) | ||
store_in_cache(req, cached_resp) | ||
return cached_resp | ||
|
||
elsif req.cacheable? && actual_resp.cacheable? | ||
store_in_cache(req, actual_resp) | ||
return actual_resp | ||
|
||
else | ||
return actual_resp | ||
end | ||
end | ||
|
||
# @return [HTTP::Response::Caching] the actual response returned | ||
# by request_performer | ||
def make_request(req, options, request_performer) | ||
req.sent_at = Time.now | ||
|
||
request_performer.call(req, options).caching.tap do |res| | ||
res.received_at = Time.now | ||
res.requested_at = req.sent_at | ||
end | ||
end | ||
|
||
# @return [HTTP::Response::Caching, nil] the cached response for the request | ||
def cache_lookup(request) | ||
return nil if request.skips_cache? | ||
|
||
rack_resp = metastore.lookup(request, entitystore) | ||
return if rack_resp.nil? | ||
|
||
HTTP::Response.new( | ||
rack_resp.status, "1.1", rack_resp.headers, stringify(rack_resp.body) | ||
).caching | ||
end | ||
|
||
# Store response in cache | ||
# | ||
# @return [nil] | ||
# | ||
# --- | ||
# | ||
# We have to convert the response body in to a string body so | ||
# that the cache store reading the body will not prevent the | ||
# original requester from doing so. | ||
def store_in_cache(request, response) | ||
response.body = response.body.to_s | ||
metastore.store(request, response, entitystore) | ||
nil | ||
end | ||
|
||
# Invalidate all response from the requested resource | ||
# | ||
# @return [nil] | ||
def invalidate_cache(request) | ||
metastore.invalidate(request, entitystore) | ||
end | ||
|
||
# Inits a new instance | ||
# | ||
# @option opts [String] :metastore URL to the metastore location | ||
# @option opts [String] :entitystore URL to the entitystore location | ||
# @option opts [Logger] :logger logger to use | ||
def initialize(opts) | ||
@metastore = storage.resolve_metastore_uri(opts.fetch(:metastore)) | ||
@entitystore = storage.resolve_entitystore_uri(opts.fetch(:entitystore)) | ||
@logger = opts.fetch(:logger) { NullLogger.new } | ||
end | ||
|
||
attr_reader :metastore, :entitystore, :logger | ||
|
||
def storage | ||
@@storage ||= Rack::Cache::Storage.new # rubocop:disable Style/ClassVars | ||
end | ||
|
||
def stringify(body) | ||
if body.respond_to?(:each) | ||
"".tap do |buf| | ||
body.each do |part| | ||
buf << part | ||
end | ||
end | ||
else | ||
body.to_s | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
require "delegate" | ||
|
||
require "http/errors" | ||
require "http/headers" | ||
|
||
module HTTP | ||
class Cache | ||
# Convenience methods around cache control headers. | ||
class Headers < ::SimpleDelegator | ||
def initialize(headers) | ||
if headers.is_a? HTTP::Headers | ||
super headers | ||
else | ||
super HTTP::Headers.coerce headers | ||
end | ||
end | ||
|
||
# @return [Boolean] does this message force revalidation | ||
def forces_revalidation? | ||
must_revalidate? || max_age == 0 | ||
end | ||
|
||
# @return [Boolean] does the cache control include 'must-revalidate' | ||
def must_revalidate? | ||
matches?(/\bmust-revalidate\b/i) | ||
end | ||
|
||
# @return [Boolean] does the cache control include 'no-cache' | ||
def no_cache? | ||
matches?(/\bno-cache\b/i) | ||
end | ||
|
||
# @return [Boolean] does the cache control include 'no-stor' | ||
def no_store? | ||
matches?(/\bno-store\b/i) | ||
end | ||
|
||
# @return [Boolean] does the cache control include 'public' | ||
def public? | ||
matches?(/\bpublic\b/i) | ||
end | ||
|
||
# @return [Boolean] does the cache control include 'private' | ||
def private? | ||
matches?(/\bprivate\b/i) | ||
end | ||
|
||
# @return [Numeric] the max number of seconds this message is | ||
# considered fresh. | ||
def max_age | ||
explicit_max_age || seconds_til_expires || Float::INFINITY | ||
end | ||
|
||
# @return [Boolean] is the vary header set to '*' | ||
def vary_star? | ||
get("Vary").any? { |v| "*" == v.strip } | ||
end | ||
|
||
private | ||
|
||
# @return [Boolean] true when cache-control header matches the pattern | ||
def matches?(pattern) | ||
get("Cache-Control").any? { |v| v =~ pattern } | ||
end | ||
|
||
# @return [Numeric] number of seconds until the time in the | ||
# expires header is reached. | ||
# | ||
# --- | ||
# Some servers send a "Expire: -1" header which must be treated as expired | ||
def seconds_til_expires | ||
get("Expires") | ||
.map { |e| http_date_to_ttl(e) } | ||
.max | ||
end | ||
|
||
def http_date_to_ttl(t_str) | ||
ttl = to_time_or_epoch(t_str) - Time.now | ||
|
||
ttl < 0 ? 0 : ttl | ||
end | ||
|
||
# @return [Time] parses t_str at a time; if that fails returns epoch time | ||
def to_time_or_epoch(t_str) | ||
Time.httpdate(t_str) | ||
rescue ArgumentError | ||
Time.at(0) | ||
end | ||
|
||
# @return [Numeric] the value of the max-age component of cache control | ||
def explicit_max_age | ||
get("Cache-Control") | ||
.map { |v| (/max-age=(\d+)/i).match(v) } | ||
.compact | ||
.map { |m| m[1].to_i } | ||
.max | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
module HTTP | ||
class Cache | ||
# NoOp cache. Always makes the request. Allows avoiding | ||
# conditionals in the request flow. | ||
class NullCache | ||
# @return [Response] the result of the provided block | ||
# @yield [request, options] so that the request can actually be made | ||
def perform(request, options) | ||
yield(request, options) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this not support
>= 50
or something like that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is. And
verify.threshold = 58.1
in conjunction withverify.require_exact_threshold = false
(above) means>= 58.1
. We bump it up to try to enforce ourselves to "keep moving forward" to the 100% API documentation coverage :D