Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
192 changes: 192 additions & 0 deletions source/extensions/filters/http/cache/cache_filter.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#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::HeaderMap& 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::HeaderMap& 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() {
// Clear decoder_callbacks_ so any pending callbacks will see that this filter is no longer
// active().
decoder_callbacks_ = nullptr;
Comment thread
toddmgreer marked this conversation as resolved.
Outdated
if (lookup_) {
lookup_->onDestroy();
}
if (insert_) {
insert_->onDestroy();
}
}

Http::FilterHeadersStatus CacheFilter::decodeHeaders(Http::HeaderMap& 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) { onHeadersAsync(std::move(result)); });
return Http::FilterHeadersStatus::StopIteration;
Comment thread
toddmgreer marked this conversation as resolved.
Outdated
}

Http::FilterHeadersStatus CacheFilter::encodeHeaders(Http::HeaderMap& 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) {
if (!active()) {
return;
}
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::HeaderMapPtr&& trailers) { onTrailersAsync(std::move(trailers)); });
}
}
}

void CacheFilter::onHeadersAsync(LookupResult&& result) {
post([this, status = result.cache_entry_status_, headers = result.headers_.release(),
response_ranges = std::move(result.response_ranges_),
content_length = result.content_length_, has_trailers = result.has_trailers_] {
onHeaders(LookupResult{status, absl::WrapUnique(headers), content_length, response_ranges,
has_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)); });
}

void CacheFilter::onBodyAsync(Buffer::InstancePtr&& body) {
post([this, body = body.release()] { onBody(absl::WrapUnique(body)); });
}

// TODO(toddmgreer): Handle downstream backpressure.
void CacheFilter::onBody(Buffer::InstancePtr&& body) {
if (!active()) {
return;
}
if (remaining_body_.empty()) {
ASSERT(false, "CacheFilter doesn't call getBody unless there's more body to get, so this is a "
"bogus callback.");
decoder_callbacks_->resetStream();
return;
}

if (!body) {
ASSERT(false, "Cache said it had a body, but isn't giving it to us.");
decoder_callbacks_->resetStream();
return;
}
Comment thread
toddmgreer marked this conversation as resolved.
Outdated

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::HeaderMapPtr&& trailers) { onTrailersAsync(std::move(trailers)); });
}
}

void CacheFilter::onTrailers(Http::HeaderMapPtr&& trailers) {
if (active()) {
decoder_callbacks_->encodeTrailers(std::move(trailers));
}
}

void CacheFilter::onTrailersAsync(Http::HeaderMapPtr&& trailers) {
post([this, trailers = trailers.release()] { onTrailers(absl::WrapUnique(trailers)); });
}

void CacheFilter::post(std::function<void()> f) const {
decoder_callbacks_->dispatcher().post(std::move(f));
}
Comment thread
toddmgreer marked this conversation as resolved.
Outdated
} // namespace Cache
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
72 changes: 72 additions & 0 deletions source/extensions/filters/http/cache/cache_filter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#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::HeaderMap& headers, bool end_stream) override;
Comment thread
toddmgreer marked this conversation as resolved.
Outdated
// Http::StreamEncoderFilter
Http::FilterHeadersStatus encodeHeaders(Http::HeaderMap& headers, bool end_stream) override;
Http::FilterDataStatus encodeData(Buffer::Instance& buffer, bool end_stream) override;

private:
void getBody();
void post(std::function<void()> f) const;
bool active() const { return decoder_callbacks_; }

void onHeaders(LookupResult&& result);
void onHeadersAsync(LookupResult&& result);
void onBody(Buffer::InstancePtr&& body);
void onBodyAsync(Buffer::InstancePtr&& body);
void onTrailers(Http::HeaderMapPtr&& trailers);
void onTrailersAsync(Http::HeaderMapPtr&& trailers);

// These don't require private access, but are members per envoy convention.
static bool isCacheableRequest(Http::HeaderMap& headers);
static bool isCacheableResponse(Http::HeaderMap& 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_;
};
using CacheFilterPtr = std::unique_ptr<CacheFilter>;
Comment thread
toddmgreer marked this conversation as resolved.
Outdated

} // namespace Cache
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
26 changes: 22 additions & 4 deletions source/extensions/filters/http/cache/http_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class AdjustedByteRange {

private:
uint64_t first_;
const uint64_t last_;
uint64_t last_;
};

inline bool operator==(const AdjustedByteRange& lhs, const AdjustedByteRange& rhs) {
Expand Down Expand Up @@ -168,7 +168,7 @@ class LookupRequest {
Key& key() { return key_; }

// Returns the subset of this request's headers that are listed in
// envoy::config::filter::http::cache::v3::CacheConfig::allowed_vary_headers. If a cache
// envoy::extensions::filters::http::cache::v3alpha::CacheConfig::allowed_vary_headers. If a cache
// storage implementation forwards lookup requests to a remote cache server that supports *vary*
// headers, that server may need to see these headers. For local implementations, it may be
// simpler to instead call makeLookupResult with each potential response.
Expand Down Expand Up @@ -231,6 +231,15 @@ class InsertContext {
// Inserts trailers into the cache.
virtual void insertTrailers(const Http::HeaderMap& trailers) PURE;

/**
* This routine is called prior to an InsertContext being destroyed. InsertContext is responsible
* for making sure that any async events are cleaned up in the context of this routine. This
* includes timers, network calls, etc. The reason there is an onDestroy() method vs. doing this
* type of cleanup in the destructor is due to the deferred deletion model that Envoy uses to
* avoid stack unwind complications. InsertContext must not invoke any callbacks after having
* onDestroy() invoked.
*/
virtual void onDestroy() PURE;
Comment thread
toddmgreer marked this conversation as resolved.
Outdated
virtual ~InsertContext() = default;
};
using InsertContextPtr = std::unique_ptr<InsertContext>;
Expand All @@ -240,8 +249,6 @@ using InsertContextPtr = std::unique_ptr<InsertContext>;
// an in-progress lookup by simply dropping the LookupContextPtr.
class LookupContext {
public:
virtual ~LookupContext() = default;

// Get the headers from the cache. It is a programming error to call this
// twice.
virtual void getHeaders(LookupHeadersCallback&& cb) PURE;
Expand All @@ -267,6 +274,17 @@ class LookupContext {
// Get the trailers from the cache. Only called if LookupResult::has_trailers
// == true.
virtual void getTrailers(LookupTrailersCallback&& cb) PURE;

/**
* This routine is called prior to an LookupContext being destroyed. LookupContext is responsible
* for making sure that any async events are cleaned up in the context of this routine. This
* includes timers, network calls, etc. The reason there is an onDestroy() method vs. doing this
* type of cleanup in the destructor is due to the deferred deletion model that Envoy uses to
* avoid stack unwind complications. LookupContext must not invoke any callbacks after having
* onDestroy() invoked.
*/
virtual void onDestroy() PURE;
virtual ~LookupContext() = default;
Comment thread
toddmgreer marked this conversation as resolved.
};
using LookupContextPtr = std::unique_ptr<LookupContext>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class SimpleLookupContext : public LookupContext {
}

const LookupRequest& request() const { return request_; }
void onDestroy() override {}

private:
SimpleHttpCache& cache_;
Expand Down Expand Up @@ -74,6 +75,8 @@ class SimpleInsertContext : public InsertContext {
NOT_IMPLEMENTED_GCOVR_EXCL_LINE; // TODO(toddmgreer): support trailers
}

void onDestroy() override {}

private:
void commit() {
committed_ = true;
Expand Down
2 changes: 2 additions & 0 deletions source/extensions/filters/http/well_known_names.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class HttpFilterNameValues {
public:
// Buffer filter
const std::string Buffer = "envoy.buffer";
// Cache filter
const std::string Cache = "envoy.filters.http.cache";
// CORS filter
const std::string Cors = "envoy.cors";
// CSRF filter
Expand Down
Loading