Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
361326d
Add CacheFilter: an HTTP caching filter based on the HttpCache plugin…
toddmgreer Feb 11, 2020
566e086
Use OnDestroy to manage async events
toddmgreer Feb 14, 2020
e57d95e
Find HttpCache implementation in factory
toddmgreer Feb 14, 2020
ecc59d9
format fix
toddmgreer Feb 14, 2020
3434039
Merge branch 'master' of github.com:envoyproxy/envoy into CacheFilter2
toddmgreer Feb 19, 2020
ce5dfad
Replace HeaderMap with more specific subtype.
toddmgreer Feb 19, 2020
0a63174
Remove unused using.
toddmgreer Feb 19, 2020
c9423af
Fix race on decoder_callbacks_, clarify comments, and fix getBody's c…
toddmgreer Feb 19, 2020
01543bc
Remove support for out of thread HttpCache implementations.
toddmgreer Feb 19, 2020
18b348b
Remove vestigial 'active' method.
toddmgreer Feb 19, 2020
a2bb906
Delete insert_ and lookup_ in onDestroy.
toddmgreer Feb 19, 2020
d866b59
Remove vestigial onDestroy
toddmgreer Feb 20, 2020
09c38cc
Fix error of calling continueDecoding before stopping iteration.
toddmgreer Feb 20, 2020
0112fd2
Test that CacheFilter can handle HttpCache implementations that both …
toddmgreer Feb 20, 2020
ee99204
Use StopIterationAndWatermark instead of StopIteration, per review co…
toddmgreer Feb 20, 2020
1b9a17f
Add factory and integration test.
toddmgreer Feb 20, 2020
590a34a
Refuse to cache requests with bodies, because they're used for a requ…
toddmgreer Feb 21, 2020
dc82ef9
Merge branch 'master' of github.com:envoyproxy/envoy into CacheFilter2
toddmgreer Feb 21, 2020
b9ad06b
Restore NiceMocks
toddmgreer Feb 21, 2020
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 source/common/common/logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace Logger {
FUNCTION(aws) \
FUNCTION(assert) \
FUNCTION(backtrace) \
FUNCTION(cache_filter) \
FUNCTION(client) \
FUNCTION(config) \
FUNCTION(connection) \
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/headers.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class HeaderValues {
const LowerCaseString AccessControlExposeHeaders{"access-control-expose-headers"};
const LowerCaseString AccessControlMaxAge{"access-control-max-age"};
const LowerCaseString AccessControlAllowCredentials{"access-control-allow-credentials"};
const LowerCaseString Age{"age"};
const LowerCaseString Authorization{"authorization"};
const LowerCaseString ProxyAuthenticate{"proxy-authenticate"};
const LowerCaseString ProxyAuthorization{"proxy-authorization"};
Expand Down Expand Up @@ -172,6 +173,7 @@ class HeaderValues {
const std::string NoCache{"no-cache"};
const std::string NoCacheMaxAge0{"no-cache, max-age=0"};
const std::string NoTransform{"no-transform"};
const std::string Private{"private"};
} CacheControlValues;

struct {
Expand Down
14 changes: 14 additions & 0 deletions source/extensions/filters/http/cache/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ load(

envoy_package()

envoy_cc_library(
name = "cache_filter_lib",
srcs = ["cache_filter.cc"],
hdrs = ["cache_filter.h"],
deps = [
":http_cache_lib",
"//source/common/common:logger_lib",
"//source/common/common:macros",
"//source/common/http:headers_lib",
"//source/extensions/filters/http/common:pass_through_filter_lib",
"@envoy_api//envoy/extensions/filters/http/cache/v3alpha:pkg_cc_proto",
],
)

envoy_proto_library(
name = "key",
srcs = ["key.proto"],
Expand Down
149 changes: 149 additions & 0 deletions source/extensions/filters/http/cache/cache_filter.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#include "extensions/filters/http/cache/cache_filter.h"

#include "common/http/headers.h"

#include "absl/strings/string_view.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace Cache {

bool CacheFilter::isCacheableRequest(Http::RequestHeaderMap& headers) {
const Http::HeaderEntry* method = headers.Method();
const Http::HeaderEntry* forwarded_proto = headers.ForwardedProto();
const Http::HeaderValues& header_values = Http::Headers::get();
// TODO(toddmgreer): Also serve HEAD requests from cache.
// TODO(toddmgreer): Check all the other cache-related headers.
return method && forwarded_proto && headers.Path() && headers.Host() &&
(method->value() == header_values.MethodValues.Get) &&
(forwarded_proto->value() == header_values.SchemeValues.Http ||
forwarded_proto->value() == header_values.SchemeValues.Https);
}

bool CacheFilter::isCacheableResponse(Http::ResponseHeaderMap& headers) {
const Http::HeaderEntry* cache_control = headers.CacheControl();
// TODO(toddmgreer): fully check for cacheability. See for example
// https://github.com/apache/incubator-pagespeed-mod/blob/master/pagespeed/kernel/http/caching_headers.h.
if (cache_control) {
return !StringUtil::caseFindToken(cache_control->value().getStringView(), ",",
Http::Headers::get().CacheControlValues.Private);
}
return false;
}

CacheFilter::CacheFilter(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig&,
const std::string&, Stats::Scope&, TimeSource& time_source,
HttpCache& http_cache)
: time_source_(time_source), cache_(http_cache) {}

void CacheFilter::onDestroy() {
lookup_ = nullptr;
insert_ = nullptr;
}

Http::FilterHeadersStatus CacheFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) {
ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders: {}", *decoder_callbacks_, headers);
if (!isCacheableRequest(headers)) {
ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders ignoring uncacheable request: {}",
*decoder_callbacks_, headers);
return Http::FilterHeadersStatus::Continue;
}
ASSERT(decoder_callbacks_);
lookup_ = cache_.makeLookupContext(LookupRequest(headers, time_source_.systemTime()));
ASSERT(lookup_);

ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders starting lookup", *decoder_callbacks_);
lookup_->getHeaders([this](LookupResult&& result) { onHeaders(std::move(result)); });
return Http::FilterHeadersStatus::StopIteration;
Comment thread
toddmgreer marked this conversation as resolved.
Outdated
}

Http::FilterHeadersStatus CacheFilter::encodeHeaders(Http::ResponseHeaderMap& headers,
bool end_stream) {
if (lookup_ && isCacheableResponse(headers)) {
ENVOY_STREAM_LOG(debug, "CacheFilter::encodeHeaders inserting headers", *encoder_callbacks_);
insert_ = cache_.makeInsertContext(std::move(lookup_));
insert_->insertHeaders(headers, end_stream);
}
return Http::FilterHeadersStatus::Continue;
}

Http::FilterDataStatus CacheFilter::encodeData(Buffer::Instance& data, bool end_stream) {
if (insert_) {
ENVOY_STREAM_LOG(debug, "CacheFilter::encodeHeaders inserting body", *encoder_callbacks_);
// TODO(toddmgreer): Wait for the cache if necessary.
insert_->insertBody(
data, [](bool) {}, end_stream);
}
return Http::FilterDataStatus::Continue;
}

void CacheFilter::onHeaders(LookupResult&& result) {
switch (result.cache_entry_status_) {
case CacheEntryStatus::RequiresValidation:
case CacheEntryStatus::FoundNotModified:
case CacheEntryStatus::UnsatisfiableRange:
NOT_IMPLEMENTED_GCOVR_EXCL_LINE; // We don't yet return or support these codes.
case CacheEntryStatus::Unusable:
decoder_callbacks_->continueDecoding();
return;
case CacheEntryStatus::Ok:
response_has_trailers_ = result.has_trailers_;
const bool end_stream = (result.content_length_ == 0 && !response_has_trailers_);
// TODO(toddmgreer): Calculate age per https://httpwg.org/specs/rfc7234.html#age.calculations
result.headers_->addReferenceKey(Http::Headers::get().Age, 0);
decoder_callbacks_->encodeHeaders(std::move(result.headers_), end_stream);
if (end_stream) {
return;
}
if (result.content_length_ > 0) {
remaining_body_.emplace_back(0, result.content_length_);
getBody();
} else {
lookup_->getTrailers(
[this](Http::ResponseTrailerMapPtr&& trailers) { onTrailers(std::move(trailers)); });
}
}
}

void CacheFilter::getBody() {
ASSERT(!remaining_body_.empty(), "No reason to call getBody when there's no body to get.");
lookup_->getBody(remaining_body_[0],
[this](Buffer::InstancePtr&& body) { onBody(std::move(body)); });
}

// TODO(toddmgreer): Handle downstream backpressure.
void CacheFilter::onBody(Buffer::InstancePtr&& body) {
ASSERT(!remaining_body_.empty(),
"CacheFilter doesn't call getBody unless there's more body to get, so this is a "
"bogus callback.");
ASSERT(body, "Cache said it had a body, but isn't giving it to us.");

const uint64_t bytes_from_cache = body->length();
if (bytes_from_cache < remaining_body_[0].length()) {
remaining_body_[0].trimFront(bytes_from_cache);
} else if (bytes_from_cache == remaining_body_[0].length()) {
remaining_body_.erase(remaining_body_.begin());
} else {
ASSERT(false, "Received oversized body from cache.");
decoder_callbacks_->resetStream();
return;
}

const bool end_stream = remaining_body_.empty() && !response_has_trailers_;
decoder_callbacks_->encodeData(*body, end_stream);
if (!remaining_body_.empty()) {
getBody();
} else if (response_has_trailers_) {
lookup_->getTrailers(
[this](Http::ResponseTrailerMapPtr&& trailers) { onTrailers(std::move(trailers)); });
}
}

void CacheFilter::onTrailers(Http::ResponseTrailerMapPtr&& trailers) {
decoder_callbacks_->encodeTrailers(std::move(trailers));
}
} // namespace Cache
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
67 changes: 67 additions & 0 deletions source/extensions/filters/http/cache/cache_filter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#pragma once

#include <functional>
#include <memory>
#include <string>
#include <vector>

#include "envoy/extensions/filters/http/cache/v3alpha/cache.pb.h"

#include "common/common/logger.h"

#include "extensions/filters/http/cache/http_cache.h"
#include "extensions/filters/http/common/pass_through_filter.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace Cache {

/**
* A filter that caches responses and attempts to satisfy requests from cache.
*/
class CacheFilter : public Http::PassThroughFilter,
public Logger::Loggable<Logger::Id::cache_filter> {
public:
CacheFilter(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config,
const std::string& stats_prefix, Stats::Scope& scope, TimeSource& time_source,
HttpCache& http_cache);
// Http::StreamFilterBase
void onDestroy() override;
// Http::StreamDecoderFilter
Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers,
bool end_stream) override;
// Http::StreamEncoderFilter
Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers,
bool end_stream) override;
Http::FilterDataStatus encodeData(Buffer::Instance& buffer, bool end_stream) override;

private:
void getBody();
void onHeaders(LookupResult&& result);
void onBody(Buffer::InstancePtr&& body);
void onTrailers(Http::ResponseTrailerMapPtr&& trailers);

// These don't require private access, but are members per envoy convention.
static bool isCacheableRequest(Http::RequestHeaderMap& headers);
static bool isCacheableResponse(Http::ResponseHeaderMap& headers);

TimeSource& time_source_;
HttpCache& cache_;
LookupContextPtr lookup_;
InsertContextPtr insert_;

// Tracks what body bytes still need to be read from the cache. This is
// currently only one Range, but will expand when full range support is added. Initialized by
// onOkHeaders.
std::vector<AdjustedByteRange> remaining_body_;

// True if the response has trailers.
// TODO(toddmgreer): cache trailers.
bool response_has_trailers_;
};

} // namespace Cache
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
15 changes: 8 additions & 7 deletions source/extensions/filters/http/cache/http_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,19 @@ std::ostream& operator<<(std::ostream& os, const AdjustedByteRange& range) {
return os << "[" << range.begin() << "," << range.end() << ")";
}

LookupRequest::LookupRequest(const Http::HeaderMap& request_headers, SystemTime timestamp)
LookupRequest::LookupRequest(const Http::RequestHeaderMap& request_headers, SystemTime timestamp)
: timestamp_(timestamp),
request_cache_control_(request_headers.CacheControl() == nullptr
? ""
: request_headers.CacheControl()->value().getStringView()) {
// These ASSERTs check prerequisites. A request without these headers can't be looked up in cache;
// CacheFilter doesn't create LookupRequests for such requests.
ASSERT(request_headers.Path(), "Can't form cache lookup key for malformed Http::HeaderMap "
ASSERT(request_headers.Path(), "Can't form cache lookup key for malformed Http::RequestHeaderMap "
"with null Path.");
ASSERT(request_headers.ForwardedProto(),
"Can't form cache lookup key for malformed Http::HeaderMap with null ForwardedProto.");
ASSERT(request_headers.Host(), "Can't form cache lookup key for malformed Http::HeaderMap "
ASSERT(
request_headers.ForwardedProto(),
"Can't form cache lookup key for malformed Http::RequestHeaderMap with null ForwardedProto.");
ASSERT(request_headers.Host(), "Can't form cache lookup key for malformed Http::RequestHeaderMap "
"with null Host.");
const Http::HeaderString& forwarded_proto = request_headers.ForwardedProto()->value();
const auto& scheme_values = Http::Headers::get().SchemeValues;
Expand All @@ -71,7 +72,7 @@ size_t stableHashKey(const Key& key) { return MessageUtil::hash(key); }
size_t localHashKey(const Key& key) { return stableHashKey(key); }

// Returns true if response_headers is fresh.
bool LookupRequest::isFresh(const Http::HeaderMap& response_headers) const {
bool LookupRequest::isFresh(const Http::ResponseHeaderMap& response_headers) const {
if (!response_headers.Date()) {
return false;
}
Expand All @@ -86,7 +87,7 @@ bool LookupRequest::isFresh(const Http::HeaderMap& response_headers) const {
return timestamp_ <= Utils::httpTime(response_headers.get(Http::Headers::get().Expires));
}

LookupResult LookupRequest::makeLookupResult(Http::HeaderMapPtr&& response_headers,
LookupResult LookupRequest::makeLookupResult(Http::ResponseHeaderMapPtr&& response_headers,
uint64_t content_length) const {
// TODO(toddmgreer): Implement all HTTP caching semantics.
ASSERT(response_headers);
Expand Down
Loading