-
Notifications
You must be signed in to change notification settings - Fork 5.5k
filter: Add CacheFilter: a pluggable HTTP caching filter #10019
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
Merged
Merged
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 566e086
Use OnDestroy to manage async events
toddmgreer e57d95e
Find HttpCache implementation in factory
toddmgreer ecc59d9
format fix
toddmgreer 3434039
Merge branch 'master' of github.com:envoyproxy/envoy into CacheFilter2
toddmgreer ce5dfad
Replace HeaderMap with more specific subtype.
toddmgreer 0a63174
Remove unused using.
toddmgreer c9423af
Fix race on decoder_callbacks_, clarify comments, and fix getBody's c…
toddmgreer 01543bc
Remove support for out of thread HttpCache implementations.
toddmgreer 18b348b
Remove vestigial 'active' method.
toddmgreer a2bb906
Delete insert_ and lookup_ in onDestroy.
toddmgreer d866b59
Remove vestigial onDestroy
toddmgreer 09c38cc
Fix error of calling continueDecoding before stopping iteration.
toddmgreer 0112fd2
Test that CacheFilter can handle HttpCache implementations that both …
toddmgreer ee99204
Use StopIterationAndWatermark instead of StopIteration, per review co…
toddmgreer 1b9a17f
Add factory and integration test.
toddmgreer 590a34a
Refuse to cache requests with bodies, because they're used for a requ…
toddmgreer dc82ef9
Merge branch 'master' of github.com:envoyproxy/envoy into CacheFilter2
toddmgreer b9ad06b
Restore NiceMocks
toddmgreer 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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,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; | ||
| } | ||
|
|
||
| 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 | ||
This file contains hidden or 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,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 |
This file contains hidden or 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.
Uh oh!
There was an error while loading. Please reload this page.