diff --git a/api/envoy/extensions/filters/http/cache/v3/cache.proto b/api/envoy/extensions/filters/http/cache/v3/cache.proto
index 70687b7150842..0613f499c86c8 100644
--- a/api/envoy/extensions/filters/http/cache/v3/cache.proto
+++ b/api/envoy/extensions/filters/http/cache/v3/cache.proto
@@ -20,7 +20,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// [#protodoc-title: HTTP Cache Filter]
// [#extension: envoy.filters.http.cache]
-// [#next-free-field: 7]
+// [#next-free-field: 8]
message CacheConfig {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.cache.v2alpha.CacheConfig";
@@ -93,4 +93,16 @@ message CacheConfig {
// causes the cache to validate with its upstream even if the lookup is a hit. Setting this
// to true will ignore these headers.
bool ignore_request_cache_control_header = 6;
+
+ // If this is set, requests sent upstream to populate the cache will go to the
+ // specified cluster rather than the cluster selected by the vhost and route.
+ //
+ // If you have actions to be taken by the router filter - either
+ // ``upstream_http_filters`` or one of the ``RouteConfiguration`` actions such as
+ // ``response_headers_to_add`` - then the cache's side-channel going directly to the
+ // routed cluster will bypass these actions. You can set ``override_upstream_cluster``
+ // to an internal listener which duplicates the relevant ``RouteConfiguration``, to
+ // replicate the desired behavior on the side-channel upstream request issued by the
+ // cache.
+ string override_upstream_cluster = 7;
}
diff --git a/changelogs/current.yaml b/changelogs/current.yaml
index 9ecf0d6e48ce5..92200502aafca 100644
--- a/changelogs/current.yaml
+++ b/changelogs/current.yaml
@@ -2,6 +2,23 @@ date: Pending
behavior_changes:
# *Changes that are expected to cause an incompatibility if applicable; deployment changes are likely required*
+- area: cache_filter
+ change: |
+ CacheFilter (WIP) has been completely reworked. Any existing cache implementations
+ will need to be modified to fit the new API. The new cache API is much simpler, as
+ individual cache implementations no longer need to comprehend various http headers,
+ only read and write at keys. Cache filter now handles "thundering herds" - if
+ multiple requests for the same resource arrive before the cache is populated,
+ now only one request goes upstream, and there is only one insert to the cache.
+ Range requests now convert to an upstream request for the entire resource, to
+ populate the cache.
+ Surprising behavior change may result if there are any active filters upstream
+ of a CacheFilter, including if RouteConfiguration does any actions
+ (e.g. adding headers) - it is recommended that for anything other than the most
+ simplistic configuration (for which the CacheFilter should be the furthest
+ upstream filter), a CacheFilter should be configured to make its requests
+ to an InternalListener which duplicates the RouteConfiguration and any filter
+ chain upstream of the CacheFilter. This is recognized as far from ideal.
minor_behavior_changes:
# *Changes that may cause incompatibilities for some users, but should not for most*
diff --git a/docs/root/_static/cache-filter-internal-listener.svg b/docs/root/_static/cache-filter-internal-listener.svg
new file mode 100644
index 0000000000000..be569c60adbaf
--- /dev/null
+++ b/docs/root/_static/cache-filter-internal-listener.svg
@@ -0,0 +1 @@
+
diff --git a/docs/root/configuration/http/http_filters/_include/http-cache-configuration-internal-listener.yaml b/docs/root/configuration/http/http_filters/_include/http-cache-configuration-internal-listener.yaml
new file mode 100644
index 0000000000000..84a8442d51149
--- /dev/null
+++ b/docs/root/configuration/http/http_filters/_include/http-cache-configuration-internal-listener.yaml
@@ -0,0 +1,113 @@
+bootstrap_extensions:
+- name: envoy.bootstrap.internal_listener
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.bootstrap.internal_listener.v3.InternalListener
+static_resources:
+ listeners:
+ - address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 8000
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.http_connection_manager
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ codec_type: AUTO
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: backend
+ response_headers_to_add:
+ - header:
+ key: x-something
+ value: something
+ domains:
+ - "*"
+ routes:
+ - match:
+ prefix: "/service/1"
+ route:
+ cluster: service1
+ - match:
+ prefix: "/service/2"
+ route:
+ cluster: service2
+ http_filters:
+ - name: "envoy.filters.http.cache"
+ typed_config:
+ "@type": "type.googleapis.com/envoy.extensions.filters.http.cache.v3.CacheConfig"
+ override_upstream_cluster: cache_internal_listener_cluster
+ typed_config:
+ "@type": "type.googleapis.com/envoy.extensions.http.cache.simple_http_cache.v3.SimpleHttpCacheConfig"
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ - name: cache_internal_listener
+ internal_listener: {}
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.http_connection_manager
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ codec_type: AUTO
+ stat_prefix: cache_internal_listener
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: backend
+ response_headers_to_add:
+ - header:
+ key: x-something
+ value: something
+ domains:
+ - "*"
+ routes:
+ - match:
+ prefix: "/service/1"
+ route:
+ cluster: service1
+ - match:
+ prefix: "/service/2"
+ route:
+ cluster: service2
+ http_filters:
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+
+ clusters:
+ - name: service1
+ type: STRICT_DNS
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: service1
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: service1
+ port_value: 8000
+ - name: service2
+ type: STRICT_DNS
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: service2
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: service2
+ port_value: 8000
+ - name: cache_internal_listener_cluster
+ load_assignment:
+ cluster_name: cache_internal_listener_cluster
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ envoy_internal_address:
+ server_listener_name: cache_internal_listener
diff --git a/docs/root/configuration/http/http_filters/cache_filter.rst b/docs/root/configuration/http/http_filters/cache_filter.rst
index 68deed12bb0a3..e804b7dc1d8a3 100644
--- a/docs/root/configuration/http/http_filters/cache_filter.rst
+++ b/docs/root/configuration/http/http_filters/cache_filter.rst
@@ -13,12 +13,25 @@ Cache filter
upstream than the cache filter, while non-cacheable requests still go through the
listener filter chain. It is therefore recommended for consistency that only the
router filter should be further upstream in the listener filter chain than the
- cache filter.
+ cache filter, and even then only if the router filter does not perform any mutations
+ such as if ``request_headers_to_add`` is set.
.. image:: /_static/cache-filter-chain.svg
:width: 80%
:align: center
+* For more complex filter chains where some filters must be upstream of the cache
+ filter for correct behavior, or if the router filter is configured to perform
+ mutations via
+ :ref:`RouteConfiguration `
+ the recommended way to configure this so that it works correctly is to configure
+ an internal listener which duplicates the part of the filter chain that is
+ upstream of the cache filter, and the ``RouteConfiguration``.
+
+.. image:: /_static/cache-filter-internal-listener.svg
+ :width: 80%
+ :align: center
+
The HTTP Cache filter implements most of the complexity of HTTP caching semantics.
For HTTP Requests:
@@ -50,6 +63,16 @@ Example filter configuration with a ``SimpleHttpCache`` cache implementation:
:lineno-start: 29
:caption: :download:`http-cache-configuration.yaml <_include/http-cache-configuration.yaml>`
+The more complicated filter chain configuration required if mutations occur upstream of the cache filter
+involves duplicating the full route config into an internal listener (unfortunately this is currently unavoidable):
+
+.. literalinclude:: _include/http-cache-configuration-internal-listener.yaml
+ :language: yaml
+ :lines: 38-113
+ :linenos:
+ :lineno-start: 38
+ :caption: :download:`http-cache-configuration-internal-listener.yaml <_include/http-cache-configuration-internal-listener.yaml>`
+
.. seealso::
:ref:`Envoy Cache Sandbox `
diff --git a/source/extensions/filters/http/cache/BUILD b/source/extensions/filters/http/cache/BUILD
index c8bb391d51cc0..c6bb70fb55305 100644
--- a/source/extensions/filters/http/cache/BUILD
+++ b/source/extensions/filters/http/cache/BUILD
@@ -12,25 +12,87 @@ licenses(["notice"]) # Apache 2
envoy_extension_package()
+envoy_cc_library(
+ name = "http_source_interface",
+ hdrs = ["http_source.h"],
+ deps = [
+ "//envoy/buffer:buffer_interface",
+ "//envoy/http:header_map_interface",
+ "//source/extensions/filters/http/cache:range_utils_lib",
+ "@com_google_absl//absl/functional:any_invocable",
+ ],
+)
+
+envoy_cc_library(
+ name = "upstream_request_lib",
+ srcs = ["upstream_request_impl.cc"],
+ hdrs = [
+ "upstream_request.h",
+ "upstream_request_impl.h",
+ ],
+ deps = [
+ ":http_source_interface",
+ ":range_utils_lib",
+ ":stats",
+ "//source/common/buffer:watermark_buffer_lib",
+ "//source/common/common:cancel_wrapper_lib",
+ "//source/common/common:logger_lib",
+ "@com_google_absl//absl/types:variant",
+ ],
+)
+
+envoy_cc_library(
+ name = "cache_sessions_lib",
+ srcs = [
+ "cache_sessions.cc",
+ ],
+ hdrs = [
+ "cache_sessions.h",
+ ],
+ deps = [
+ ":http_cache_lib",
+ ":stats",
+ ":upstream_request_lib",
+ "//source/common/http:utility_lib",
+ ],
+)
+
+envoy_cc_library(
+ name = "cache_sessions_impl_lib",
+ srcs = [
+ "cache_sessions_impl.cc",
+ ],
+ hdrs = [
+ "cache_sessions_impl.h",
+ ],
+ deps = [
+ ":cache_sessions_lib",
+ ":cacheability_utils_lib",
+ ":upstream_request_lib",
+ "//source/common/common:cancel_wrapper_lib",
+ ],
+)
+
envoy_cc_library(
name = "cache_filter_lib",
srcs = [
"cache_filter.cc",
- "upstream_request.cc",
],
hdrs = [
"cache_filter.h",
- "filter_state.h",
- "upstream_request.h",
],
deps = [
":cache_custom_headers",
":cache_entry_utils_lib",
- ":cache_filter_logging_info_lib",
":cache_headers_utils_lib",
- ":cache_insert_queue_lib",
+ ":cache_sessions_impl_lib",
+ ":cache_sessions_lib",
":cacheability_utils_lib",
":http_cache_lib",
+ ":stats",
+ ":upstream_request_lib",
+ "//source/common/buffer:buffer_lib",
+ "//source/common/common:cancel_wrapper_lib",
"//source/common/common:enum_to_int",
"//source/common/common:logger_lib",
"//source/common/common:macros",
@@ -65,16 +127,6 @@ envoy_cc_library(
],
)
-envoy_cc_library(
- name = "cache_insert_queue_lib",
- srcs = ["cache_insert_queue.cc"],
- hdrs = ["cache_insert_queue.h"],
- deps = [
- ":http_cache_lib",
- "//source/common/buffer:buffer_lib",
- ],
-)
-
envoy_cc_library(
name = "cache_policy_lib",
hdrs = ["cache_policy.h"],
@@ -82,7 +134,6 @@ envoy_cc_library(
":cache_headers_utils_lib",
":http_cache_lib",
"//source/common/http:header_map_lib",
- "//source/common/stream_info:filter_state_lib",
],
)
@@ -91,6 +142,15 @@ envoy_proto_library(
srcs = ["key.proto"],
)
+envoy_cc_library(
+ name = "cache_progress_receiver_interface",
+ hdrs = ["cache_progress_receiver.h"],
+ deps = [
+ "//envoy/http:header_map_interface",
+ "//source/extensions/filters/http/cache:range_utils_lib",
+ ],
+)
+
envoy_cc_library(
name = "http_cache_lib",
srcs = ["http_cache.cc"],
@@ -99,6 +159,8 @@ envoy_cc_library(
":cache_custom_headers",
":cache_entry_utils_lib",
":cache_headers_utils_lib",
+ ":cache_progress_receiver_interface",
+ ":http_source_interface",
":key_cc_proto",
":range_utils_lib",
"//envoy/buffer:buffer_interface",
@@ -137,6 +199,7 @@ envoy_cc_library(
hdrs = ["cache_headers_utils.h"],
deps = [
":cache_custom_headers",
+ ":key_cc_proto",
"//envoy/common:time_interface",
"//envoy/http:header_map_interface",
"//source/common/common:matchers_lib",
@@ -159,12 +222,12 @@ envoy_cc_library(
],
)
-envoy_cc_library(
- name = "cache_filter_logging_info_lib",
- srcs = ["cache_filter_logging_info.cc"],
- hdrs = ["cache_filter_logging_info.h"],
+envoy_cc_extension(
+ name = "stats",
+ srcs = ["stats.cc"],
+ hdrs = ["stats.h"],
deps = [
- "//source/common/stream_info:filter_state_lib",
+ ":cache_entry_utils_lib",
],
)
@@ -174,6 +237,8 @@ envoy_cc_extension(
hdrs = ["config.h"],
deps = [
":cache_filter_lib",
+ ":cache_sessions_lib",
+ ":stats",
"//source/extensions/filters/http/common:factory_base_lib",
"@envoy_api//envoy/extensions/filters/http/cache/v3:pkg_cc_proto",
],
diff --git a/source/extensions/filters/http/cache/cache_entry_utils.cc b/source/extensions/filters/http/cache/cache_entry_utils.cc
index dd5eb27120bd5..948e37ea58fe9 100644
--- a/source/extensions/filters/http/cache/cache_entry_utils.cc
+++ b/source/extensions/filters/http/cache/cache_entry_utils.cc
@@ -9,16 +9,26 @@ namespace Cache {
absl::string_view cacheEntryStatusString(CacheEntryStatus s) {
switch (s) {
- case CacheEntryStatus::Ok:
- return "Ok";
- case CacheEntryStatus::Unusable:
- return "Unusable";
- case CacheEntryStatus::RequiresValidation:
- return "RequiresValidation";
+ case CacheEntryStatus::Hit:
+ return "Hit";
+ case CacheEntryStatus::Miss:
+ return "Miss";
+ case CacheEntryStatus::Follower:
+ return "Follower";
+ case CacheEntryStatus::Uncacheable:
+ return "Uncacheable";
+ case CacheEntryStatus::Validated:
+ return "Validated";
+ case CacheEntryStatus::ValidatedFree:
+ return "ValidatedFree";
+ case CacheEntryStatus::FailedValidation:
+ return "FailedValidation";
case CacheEntryStatus::FoundNotModified:
return "FoundNotModified";
case CacheEntryStatus::LookupError:
return "LookupError";
+ case CacheEntryStatus::UpstreamReset:
+ return "UpstreamReset";
}
IS_ENVOY_BUG(absl::StrCat("Unexpected CacheEntryStatus: ", s));
return "UnexpectedCacheEntryStatus";
diff --git a/source/extensions/filters/http/cache/cache_entry_utils.h b/source/extensions/filters/http/cache/cache_entry_utils.h
index d43c4f6a4198d..42f0e283f0618 100644
--- a/source/extensions/filters/http/cache/cache_entry_utils.h
+++ b/source/extensions/filters/http/cache/cache_entry_utils.h
@@ -22,23 +22,37 @@ struct ResponseMetadata {
// calculations at: https://httpwg.org/specs/rfc7234.html#age.calculations
Envoy::SystemTime response_time_;
};
-using ResponseMetadataPtr = std::unique_ptr;
// Whether a given cache entry is good for the current request.
enum class CacheEntryStatus {
// This entry is fresh, and an appropriate response to the request.
- Ok,
- // No usable entry was found. If this was generated for a cache entry, the
- // cache should delete that entry.
- Unusable,
- // This entry is stale, but appropriate for validating
- RequiresValidation,
+ Hit,
+ // The request was cacheable and was not already in the cache. This also means
+ // the cache was populated by this request.
+ Miss,
+ // The entry was being inserted when this request was made - it's like a
+ // hit, but streamed from the same request as the original "Miss", so still
+ // potentially subject to upstream reset because the cache entry isn't fully
+ // populated yet.
+ Follower,
+ // The request was not cacheable. All matching requests will go to the
+ // upstream.
+ Uncacheable,
+ // This entry required validation, and validated successfully.
+ Validated,
+ // This entry required validation while another entry was already validating,
+ // so it validated successfully without its own lookup.
+ ValidatedFree,
+ // This entry required validation, and did not validate.
+ FailedValidation,
// This entry is fresh, and an appropriate basis for a 304 Not Modified
// response.
FoundNotModified,
// The cache lookup failed, e.g. because the cache was unreachable or an RPC
- // timed out. The caller shouldn't use this lookup's context for an insert.
+ // timed out. Mostly behaves the same as Uncacheable but may retry each time.
LookupError,
+ // The cache attempted to read from upstream for insert, but upstream reset.
+ UpstreamReset,
};
absl::string_view cacheEntryStatusString(CacheEntryStatus s);
diff --git a/source/extensions/filters/http/cache/cache_filter.cc b/source/extensions/filters/http/cache/cache_filter.cc
index 0ca04ed2131d0..8aaece62955d0 100644
--- a/source/extensions/filters/http/cache/cache_filter.cc
+++ b/source/extensions/filters/http/cache/cache_filter.cc
@@ -2,14 +2,13 @@
#include "envoy/http/header_map.h"
+#include "source/common/buffer/buffer_impl.h"
#include "source/common/common/enum_to_int.h"
#include "source/common/http/headers.h"
#include "source/common/http/utility.h"
-#include "source/extensions/filters/http/cache/cache_custom_headers.h"
#include "source/extensions/filters/http/cache/cache_entry_utils.h"
-#include "source/extensions/filters/http/cache/cache_filter_logging_info.h"
#include "source/extensions/filters/http/cache/cacheability_utils.h"
-#include "source/extensions/filters/http/cache/upstream_request.h"
+#include "source/extensions/filters/http/cache/upstream_request_impl.h"
#include "absl/memory/memory.h"
#include "absl/strings/str_cat.h"
@@ -20,6 +19,8 @@ namespace Extensions {
namespace HttpFilters {
namespace Cache {
+using CancelWrapper::cancelWrapped;
+
namespace {
// This value is only used if there is no encoderBufferLimit on the stream;
// without *some* constraint here, a very large chunk can be requested and
@@ -29,52 +30,62 @@ namespace {
// behavioral change when a constraint is added.
//
// And everyone knows 64MB should be enough for anyone.
-static const size_t MAX_BYTES_TO_FETCH_FROM_CACHE_PER_REQUEST = 64 * 1024 * 1024;
+static constexpr size_t MaxBytesToFetchFromCachePerRead = 64 * 1024 * 1024;
} // namespace
-struct CacheResponseCodeDetailValues {
- const absl::string_view ResponseFromCacheFilter = "cache.response_from_cache_filter";
-};
-
-using CacheResponseCodeDetails = ConstSingleton;
+namespace CacheResponseCodeDetails {
+static constexpr absl::string_view ResponseFromCacheFilter = "cache.response_from_cache_filter";
+static constexpr absl::string_view CacheFilterInsert = "cache.insert_via_upstream";
+static constexpr absl::string_view CacheFilterAbortedDuringLookup = "cache.aborted_lookup";
+static constexpr absl::string_view CacheFilterAbortedDuringHeaders = "cache.aborted_headers";
+static constexpr absl::string_view CacheFilterAbortedDuringBody = "cache.aborted_body";
+static constexpr absl::string_view CacheFilterAbortedDuringTrailers = "cache.aborted_trailers";
+} // namespace CacheResponseCodeDetails
CacheFilterConfig::CacheFilterConfig(
const envoy::extensions::filters::http::cache::v3::CacheConfig& config,
+ std::shared_ptr cache_sessions,
Server::Configuration::CommonFactoryContext& context)
: vary_allow_list_(config.allowed_vary_headers(), context), time_source_(context.timeSource()),
ignore_request_cache_control_header_(config.ignore_request_cache_control_header()),
- cluster_manager_(context.clusterManager()) {}
+ cluster_manager_(context.clusterManager()), cache_sessions_(std::move(cache_sessions)),
+ override_upstream_cluster_(config.override_upstream_cluster()) {}
+
+bool CacheFilterConfig::isCacheableResponse(const Http::ResponseHeaderMap& headers) const {
+ return CacheabilityUtils::isCacheableResponse(headers, vary_allow_list_);
+}
-CacheFilter::CacheFilter(std::shared_ptr config,
- std::shared_ptr http_cache)
- : cache_(http_cache), config_(config) {}
+CacheFilter::CacheFilter(std::shared_ptr config) : config_(config) {}
+
+void CacheFilter::setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) {
+ callbacks.addDownstreamWatermarkCallbacks(*this);
+ PassThroughFilter::setDecoderFilterCallbacks(callbacks);
+}
void CacheFilter::onDestroy() {
- filter_state_ = FilterState::Destroyed;
- if (lookup_ != nullptr) {
- lookup_->onDestroy();
- }
- if (upstream_request_ != nullptr) {
- upstream_request_->disconnectFilter();
- upstream_request_ = nullptr;
+ is_destroyed_ = true;
+ if (cancel_in_flight_callback_) {
+ cancel_in_flight_callback_();
}
+ lookup_result_.reset();
}
-void CacheFilter::sendUpstreamRequest(Http::RequestHeaderMap& request_headers) {
+absl::optional CacheFilter::clusterName() {
Router::RouteConstSharedPtr route = decoder_callbacks_->route();
const Router::RouteEntry* route_entry = (route == nullptr) ? nullptr : route->routeEntry();
if (route_entry == nullptr) {
- return sendNoRouteResponse();
+ return absl::nullopt;
}
+ return route_entry->clusterName();
+}
+
+OptRef CacheFilter::asyncClient(absl::string_view cluster_name) {
Upstream::ThreadLocalCluster* thread_local_cluster =
- config_->clusterManager().getThreadLocalCluster(route_entry->clusterName());
+ config_->clusterManager().getThreadLocalCluster(cluster_name);
if (thread_local_cluster == nullptr) {
- return sendNoClusterResponse(route_entry->clusterName());
+ return absl::nullopt;
}
- upstream_request_ =
- UpstreamRequest::create(this, std::move(lookup_), std::move(lookup_result_), cache_,
- thread_local_cluster->httpAsyncClient(), config_->upstreamOptions());
- upstream_request_->sendHeaders(request_headers);
+ return thread_local_cluster->httpAsyncClient();
}
void CacheFilter::sendNoRouteResponse() {
@@ -89,275 +100,308 @@ void CacheFilter::sendNoClusterResponse(absl::string_view cluster_name) {
"cache_no_cluster");
}
-void CacheFilter::onStreamComplete() {
- LookupStatus lookup_status = lookupStatus();
- InsertStatus insert_status = insertStatus();
- decoder_callbacks_->streamInfo().filterState()->setData(
- CacheFilterLoggingInfo::FilterStateKey,
- std::make_shared(lookup_status, insert_status),
- StreamInfo::FilterState::StateType::ReadOnly);
-}
-
Http::FilterHeadersStatus CacheFilter::decodeHeaders(Http::RequestHeaderMap& headers,
bool end_stream) {
- if (!cache_) {
- filter_state_ = FilterState::NotServingFromCache;
+ ASSERT(decoder_callbacks_);
+ if (!config_->hasCache()) {
return Http::FilterHeadersStatus::Continue;
}
- ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders: {}", *decoder_callbacks_, headers);
if (!end_stream) {
- ENVOY_STREAM_LOG(
- debug,
- "CacheFilter::decodeHeaders ignoring request because it has body and/or trailers: {}",
- *decoder_callbacks_, headers);
- filter_state_ = FilterState::NotServingFromCache;
+ ENVOY_STREAM_LOG(debug,
+ "CacheFilter::decodeHeaders ignoring request because it has body and/or "
+ "trailers: headers={}",
+ *decoder_callbacks_, headers);
+ stats().incForStatus(CacheEntryStatus::Uncacheable);
return Http::FilterHeadersStatus::Continue;
}
- if (!CacheabilityUtils::canServeRequestFromCache(headers)) {
- ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders ignoring uncacheable request: {}",
- *decoder_callbacks_, headers);
- filter_state_ = FilterState::NotServingFromCache;
- insert_status_ = InsertStatus::NoInsertRequestNotCacheable;
+ absl::Status can_serve = CacheabilityUtils::canServeRequestFromCache(headers);
+ if (!can_serve.ok()) {
+ ENVOY_STREAM_LOG(debug,
+ "CacheFilter::decodeHeaders ignoring uncacheable request: {}\nheaders={}",
+ *decoder_callbacks_, can_serve, headers);
+ stats().incForStatus(CacheEntryStatus::Uncacheable);
return Http::FilterHeadersStatus::Continue;
}
- ASSERT(decoder_callbacks_);
+ ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders: {}", *decoder_callbacks_, headers);
- LookupRequest lookup_request(headers, config_->timeSource().systemTime(),
- config_->varyAllowList(),
- config_->ignoreRequestCacheControlHeader());
- request_allows_inserts_ = !lookup_request.requestCacheControl().no_store_;
+ absl::optional original_cluster_name = clusterName();
+ absl::string_view cluster_name;
+ if (config_->overrideUpstreamCluster().empty()) {
+ if (!original_cluster_name) {
+ sendNoRouteResponse();
+ return Http::FilterHeadersStatus::StopIteration;
+ }
+ cluster_name = *original_cluster_name;
+ } else {
+ cluster_name = config_->overrideUpstreamCluster();
+ if (!original_cluster_name) {
+ // It's possible the destination cluster will only be determined further upstream in
+ // the cache filter's side-channel, in which case we can't use it in the key;
+ // in this case use "unknown" instead.
+ original_cluster_name = "unknown";
+ }
+ }
+ OptRef async_client = asyncClient(cluster_name);
+ if (!async_client) {
+ sendNoClusterResponse(cluster_name);
+ return Http::FilterHeadersStatus::StopIteration;
+ }
+ auto upstream_request_factory = std::make_unique(
+ decoder_callbacks_->dispatcher(), *async_client, config_->upstreamOptions());
+ auto lookup_request = std::make_unique(
+ headers, std::move(upstream_request_factory), *original_cluster_name,
+ decoder_callbacks_->dispatcher(), config_->timeSource().systemTime(), config_, config_,
+ config_->ignoreRequestCacheControlHeader());
is_head_request_ = headers.getMethodValue() == Http::Headers::get().MethodValues.Head;
- lookup_ = cache_->makeLookupContext(std::move(lookup_request), *decoder_callbacks_);
-
- ASSERT(lookup_);
- getHeaders(headers);
ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders starting lookup", *decoder_callbacks_);
+ config_->cacheSessions().lookup(
+ std::move(lookup_request),
+ cancelWrapped(
+ [this](ActiveLookupResultPtr lookup_result) { onLookupResult(std::move(lookup_result)); },
+ &cancel_in_flight_callback_));
+
+ // Stop the decoding stream.
+ return Http::FilterHeadersStatus::StopIteration;
+}
- // Stop the decoding stream until the cache lookup result is ready.
- return Http::FilterHeadersStatus::StopAllIterationAndWatermark;
+static absl::string_view responseCodeDetailsFromStatus(CacheEntryStatus status) {
+ switch (status) {
+ case CacheEntryStatus::Miss:
+ case CacheEntryStatus::FailedValidation:
+ return CacheResponseCodeDetails::CacheFilterInsert;
+ case CacheEntryStatus::Hit:
+ case CacheEntryStatus::FoundNotModified:
+ case CacheEntryStatus::Follower:
+ case CacheEntryStatus::Validated:
+ case CacheEntryStatus::ValidatedFree:
+ case CacheEntryStatus::UpstreamReset:
+ return CacheResponseCodeDetails::ResponseFromCacheFilter;
+ case CacheEntryStatus::Uncacheable:
+ case CacheEntryStatus::LookupError:
+ break;
+ }
+ return StreamInfo::ResponseCodeDetails::get().ViaUpstream;
}
-void CacheFilter::onUpstreamRequestComplete() { upstream_request_ = nullptr; }
+void CacheFilter::onLookupResult(ActiveLookupResultPtr lookup_result) {
+ ASSERT(lookup_result != nullptr, "lookup result should always be non-null");
+ lookup_result_ = std::move(lookup_result);
+ if (!lookup_result_->http_source_) {
+ // Lookup failed, typically implying upstream request was reset.
+ decoder_callbacks_->streamInfo().setResponseCodeDetails(
+ CacheResponseCodeDetails::CacheFilterAbortedDuringLookup);
+ decoder_callbacks_->resetStream();
+ return;
+ }
-void CacheFilter::onUpstreamRequestReset() {
- upstream_request_ = nullptr;
- decoder_callbacks_->sendLocalReply(Http::Code::ServiceUnavailable, "", nullptr, absl::nullopt,
- "cache_upstream_reset");
+ stats().incForStatus(lookup_result_->status_);
+ if (lookup_result_->status_ != CacheEntryStatus::Uncacheable) {
+ decoder_callbacks_->streamInfo().setResponseFlag(
+ StreamInfo::CoreResponseFlag::ResponseFromCacheFilter);
+ }
+
+ ENVOY_STREAM_LOG(debug, "CacheFilter calling getHeaders", *decoder_callbacks_);
+ lookup_result_->http_source_->getHeaders(cancelWrapped(
+ [this](Http::ResponseHeaderMapPtr response_headers, EndStream end_stream_enum) {
+ onHeaders(std::move(response_headers), end_stream_enum);
+ },
+ &cancel_in_flight_callback_));
}
Http::FilterHeadersStatus CacheFilter::encodeHeaders(Http::ResponseHeaderMap& headers, bool) {
- if (filter_state_ == FilterState::ServingFromCache) {
- // This call was invoked during decoding by decoder_callbacks_->encodeHeaders because a fresh
- // cached response was found and is being added to the encoding stream -- ignore it.
- return Http::FilterHeadersStatus::Continue;
- }
-
- // If lookup_ is null, the request wasn't cacheable, so the response isn't either.
- if (!lookup_) {
+ if (lookup_result_) {
+ // This call was invoked during decoding by decoder_callbacks_->encodeHeaders with data
+ // either read from the upstream via the cache filter, or from the cache.
return Http::FilterHeadersStatus::Continue;
}
-
- if (lookup_result_ == nullptr) {
- // Filter chain iteration is paused while a lookup is outstanding, but the filter chain manager
- // can still generate a local reply. One case where this can happen is when a downstream idle
- // timeout fires, which may mean that the HttpCache isn't correctly setting deadlines on its
- // asynchronous operations or is otherwise getting stuck.
- ENVOY_BUG(Http::Utility::getResponseStatus(headers) !=
- Envoy::enumToInt(Http::Code::RequestTimeout),
- "Request timed out while cache lookup was outstanding.");
- // Cancel the lookup since it's now not useful.
- lookup_->onDestroy();
- lookup_ = nullptr;
+ if (!cancel_in_flight_callback_) {
+ // If there was no lookup result and there's no request in flight, this implies
+ // no request was sent, so we must be in a pass-through configuration (either no
+ // cache or the request had a body).
return Http::FilterHeadersStatus::Continue;
}
- IS_ENVOY_BUG("encodeHeaders should not be called except under the conditions handled above");
+ // Filter chain iteration is paused while a lookup is outstanding, but the filter chain manager
+ // can still generate a local reply. One case where this can happen is when a downstream idle
+ // timeout fires, which may mean that the HttpCache isn't correctly setting deadlines on its
+ // asynchronous operations or is otherwise getting stuck.
+ ENVOY_BUG(Http::Utility::getResponseStatus(headers) !=
+ Envoy::enumToInt(Http::Code::RequestTimeout),
+ "Request timed out while cache lookup was outstanding.");
+ // Cancel the lookup since it's now not useful.
+ ASSERT(cancel_in_flight_callback_);
+ cancel_in_flight_callback_();
return Http::FilterHeadersStatus::Continue;
}
-/*static*/ LookupStatus
-CacheFilter::resolveLookupStatus(absl::optional cache_entry_status,
- FilterState filter_state) {
- if (cache_entry_status.has_value()) {
- switch (cache_entry_status.value()) {
- case CacheEntryStatus::Ok:
- return LookupStatus::CacheHit;
- case CacheEntryStatus::Unusable:
- return LookupStatus::CacheMiss;
- case CacheEntryStatus::RequiresValidation: {
- // The CacheFilter sent the response upstream for validation; check the
- // filter state to see whether and how the upstream responded. The
- // filter currently won't send the stale entry if it can't reach the
- // upstream or if the upstream responds with a 5xx, so don't include
- // special handling for those cases.
- switch (filter_state) {
- case FilterState::ValidatingCachedResponse:
- return LookupStatus::RequestIncomplete;
- case FilterState::ServingFromCache:
- ABSL_FALLTHROUGH_INTENDED;
- case FilterState::ResponseServedFromCache:
- // Functionally a cache hit, this is differentiated for metrics reporting.
- return LookupStatus::StaleHitWithSuccessfulValidation;
- case FilterState::NotServingFromCache:
- return LookupStatus::StaleHitWithFailedValidation;
- case FilterState::Initial:
- ABSL_FALLTHROUGH_INTENDED;
- case FilterState::Destroyed:
- IS_ENVOY_BUG(absl::StrCat("Unexpected filter state in requestCacheStatus: cache lookup "
- "response required validation, but filter state is ",
- filter_state));
- }
- return LookupStatus::Unknown;
- }
- case CacheEntryStatus::FoundNotModified:
- // TODO(capoferro): Report this as a FoundNotModified when we handle
- // those.
- return LookupStatus::CacheHit;
- case CacheEntryStatus::LookupError:
- return LookupStatus::LookupError;
+void CacheFilter::getBody() {
+ ASSERT(lookup_result_, "CacheFilter is trying to call getBody with no LookupResult");
+ get_body_loop_ = GetBodyLoop::Again;
+ while (get_body_loop_ == GetBodyLoop::Again) {
+ ASSERT(!remaining_ranges_.empty(), "No reason to call getBody when there's no body to get.");
+
+ // We don't want to request more than a buffer-size at a time from the cache.
+ uint64_t fetch_size_limit = encoder_callbacks_->encoderBufferLimit();
+ // If there is no buffer size limit, we still want *some* constraint.
+ if (fetch_size_limit == 0) {
+ fetch_size_limit = MaxBytesToFetchFromCachePerRead;
}
- IS_ENVOY_BUG(absl::StrCat(
- "Unhandled CacheEntryStatus encountered when retrieving request cache status: " +
- std::to_string(static_cast(filter_state))));
- return LookupStatus::Unknown;
- }
- // Either decodeHeaders decided not to do a cache lookup (because the
- // request isn't cacheable), or decodeHeaders hasn't been called yet.
- switch (filter_state) {
- case FilterState::Initial:
- return LookupStatus::RequestIncomplete;
- case FilterState::NotServingFromCache:
- return LookupStatus::RequestNotCacheable;
- // Ignore the following lines. This code should not be executed.
- // GCOV_EXCL_START
- case FilterState::ValidatingCachedResponse:
- ABSL_FALLTHROUGH_INTENDED;
- case FilterState::ServingFromCache:
- ABSL_FALLTHROUGH_INTENDED;
- case FilterState::ResponseServedFromCache:
- ABSL_FALLTHROUGH_INTENDED;
- case FilterState::Destroyed:
- ENVOY_LOG(error, absl::StrCat("Unexpected filter state in requestCacheStatus: "
- "lookup_result_ is empty but filter state is ",
- filter_state));
- }
- return LookupStatus::Unknown;
+ AdjustedByteRange fetch_range = {remaining_ranges_[0].begin(),
+ (remaining_ranges_[0].length() > fetch_size_limit)
+ ? (remaining_ranges_[0].begin() + fetch_size_limit)
+ : remaining_ranges_[0].end()};
+
+ ENVOY_STREAM_LOG(debug, "CacheFilter calling getBody", *decoder_callbacks_);
+ get_body_loop_ = GetBodyLoop::InCallback;
+ lookup_result_->http_source_->getBody(
+ fetch_range, cancelWrapped(
+ [this, &dispatcher = decoder_callbacks_->dispatcher()](
+ Buffer::InstancePtr&& body, EndStream end_stream_enum) {
+ if (onBody(std::move(body), end_stream_enum)) {
+ if (get_body_loop_ == GetBodyLoop::InCallback) {
+ // If the callback was called inline, loop it.
+ get_body_loop_ = GetBodyLoop::Again;
+ } else {
+ // If the callback was posted we're not in the loop
+ // any more, so getBody to enter the loop.
+ getBody();
+ }
+ }
+ },
+ &cancel_in_flight_callback_));
+ }
+ get_body_loop_ = GetBodyLoop::Idle;
}
-void CacheFilter::getHeaders(Http::RequestHeaderMap& request_headers) {
- ASSERT(lookup_, "CacheFilter is trying to call getHeaders with no LookupContext");
- callback_called_directly_ = true;
- lookup_->getHeaders([this, &request_headers, &dispatcher = decoder_callbacks_->dispatcher()](
- LookupResult&& result, bool end_stream) {
- ASSERT(!callback_called_directly_ && dispatcher.isThreadSafe(),
- "caches must post the callback to the filter's dispatcher");
- onHeaders(std::move(result), request_headers, end_stream);
- });
- callback_called_directly_ = false;
+void CacheFilter::getTrailers() {
+ ASSERT(lookup_result_, "CacheFilter is trying to call getTrailers with no LookupResult");
+
+ lookup_result_->http_source_->getTrailers(cancelWrapped(
+ [this, &dispatcher = decoder_callbacks_->dispatcher()](Http::ResponseTrailerMapPtr&& trailers,
+ EndStream end_stream_enum) {
+ ASSERT(
+ dispatcher.isThreadSafe(),
+ "caches must ensure the callback is called from the original thread, either by posting "
+ "to dispatcher or by calling directly");
+ onTrailers(std::move(trailers), end_stream_enum);
+ },
+ &cancel_in_flight_callback_));
}
-void CacheFilter::getBody() {
- ASSERT(lookup_, "CacheFilter is trying to call getBody with no LookupContext");
- ASSERT(!remaining_ranges_.empty(), "No reason to call getBody when there's no body to get.");
-
- // We don't want to request more than a buffer-size at a time from the cache.
- uint64_t fetch_size_limit = encoder_callbacks_->encoderBufferLimit();
- // If there is no buffer size limit, we still want *some* constraint.
- if (fetch_size_limit == 0) {
- fetch_size_limit = MAX_BYTES_TO_FETCH_FROM_CACHE_PER_REQUEST;
- }
- AdjustedByteRange fetch_range = {remaining_ranges_[0].begin(),
- (remaining_ranges_[0].length() > fetch_size_limit)
- ? (remaining_ranges_[0].begin() + fetch_size_limit)
- : remaining_ranges_[0].end()};
-
- callback_called_directly_ = true;
- lookup_->getBody(fetch_range, [this, &dispatcher = decoder_callbacks_->dispatcher()](
- Buffer::InstancePtr&& body, bool end_stream) {
- ASSERT(!callback_called_directly_ && dispatcher.isThreadSafe(),
- "caches must post the callback to the filter's dispatcher");
- onBody(std::move(body), end_stream);
- });
- callback_called_directly_ = false;
+static AdjustedByteRange rangeFromHeaders(Http::ResponseHeaderMap& response_headers) {
+ if (Http::Utility::getResponseStatus(response_headers) !=
+ static_cast(Envoy::Http::Code::PartialContent)) {
+ // Don't use content-length; we can just request *all the body* from
+ // the source and it will tell us when it gets to the end.
+ return {0, std::numeric_limits::max()};
+ }
+ Http::HeaderMap::GetResult content_range_result =
+ response_headers.get(Envoy::Http::Headers::get().ContentRange);
+ if (content_range_result.empty()) {
+ return {0, std::numeric_limits::max()};
+ }
+ absl::string_view content_range = content_range_result[0]->value().getStringView();
+ if (!absl::ConsumePrefix(&content_range, "bytes ")) {
+ return {0, std::numeric_limits::max()};
+ }
+ if (absl::ConsumePrefix(&content_range, "*/")) {
+ uint64_t len;
+ if (absl::SimpleAtoi(content_range, &len)) {
+ return {0, len};
+ }
+ return {0, std::numeric_limits::max()};
+ }
+ std::pair range_of = absl::StrSplit(content_range, '/');
+ std::pair range = absl::StrSplit(range_of.first, '-');
+ uint64_t begin, end;
+ if (!absl::SimpleAtoi(range.first, &begin)) {
+ begin = 0;
+ }
+ if (!absl::SimpleAtoi(range.second, &end)) {
+ end = std::numeric_limits::max();
+ } else {
+ end++;
+ }
+ return {begin, end};
}
-void CacheFilter::getTrailers() {
- ASSERT(lookup_, "CacheFilter is trying to call getTrailers with no LookupContext");
-
- callback_called_directly_ = true;
- lookup_->getTrailers([this, &dispatcher = decoder_callbacks_->dispatcher()](
- Http::ResponseTrailerMapPtr&& trailers) {
- ASSERT(!callback_called_directly_ && dispatcher.isThreadSafe(),
- "caches must post the callback to the filter's dispatcher");
- onTrailers(std::move(trailers));
- });
- callback_called_directly_ = false;
-}
+void CacheFilter::onHeaders(Http::ResponseHeaderMapPtr response_headers,
+ EndStream end_stream_enum) {
+ ASSERT(lookup_result_, "onHeaders should not be called with no LookupResult");
-void CacheFilter::onHeaders(LookupResult&& result, Http::RequestHeaderMap& request_headers,
- bool end_stream) {
- if (filter_state_ == FilterState::Destroyed) {
- // The filter is being destroyed, any callbacks should be ignored.
+ if (end_stream_enum == EndStream::Reset) {
+ decoder_callbacks_->streamInfo().setResponseCodeDetails(
+ CacheResponseCodeDetails::CacheFilterAbortedDuringHeaders);
+ decoder_callbacks_->resetStream();
return;
}
- if (filter_state_ == FilterState::NotServingFromCache) {
- // A response was injected into the filter chain before the cache lookup finished, e.g. because
- // the request stream timed out.
- return;
+ ASSERT(response_headers != nullptr);
+
+ if (lookup_result_->status_ == CacheEntryStatus::Miss ||
+ lookup_result_->status_ == CacheEntryStatus::Validated ||
+ lookup_result_->status_ == CacheEntryStatus::ValidatedFree) {
+ // CacheSessions adds an age header indiscriminately because once it has
+ // handed off it doesn't remember which request is associated with the insert.
+ // So here we remove that header for the non-cache response and the validated
+ // response.
+ response_headers->remove(Envoy::Http::CustomHeaders::get().Age);
}
- // TODO(yosrym93): Handle request only-if-cached directive
- lookup_result_ = std::make_unique(std::move(result));
- cache_entry_status_ = lookup_result_->cache_entry_status_;
- switch (cache_entry_status_.value()) {
- case CacheEntryStatus::FoundNotModified:
- PANIC("unsupported code");
- case CacheEntryStatus::RequiresValidation:
- // If a cache entry requires validation, inject validation headers in the
- // request and let it pass through as if no cache entry was found. If the
- // cache entry was valid, the response status should be 304 (unmodified)
- // and the cache entry will be injected in the response body.
- handleCacheHitWithValidation(request_headers);
- return;
- case CacheEntryStatus::Ok:
- if (lookup_result_->range_details_.has_value()) {
- handleCacheHitWithRangeRequest();
- return;
- }
- handleCacheHit(/* end_stream_after_headers = */ end_stream);
- return;
- case CacheEntryStatus::Unusable:
- sendUpstreamRequest(request_headers);
+ static const std::string partial_content = std::to_string(enumToInt(Http::Code::PartialContent));
+ if (response_headers->getStatusValue() == partial_content) {
+ is_partial_response_ = true;
+ }
+
+ bool end_stream = ((end_stream_enum == EndStream::End) || is_head_request_);
+
+ if (!end_stream) {
+ remaining_ranges_ = {rangeFromHeaders(*response_headers)};
+ ENVOY_STREAM_LOG(debug, "CacheFilter requesting range {}-{} {}", *decoder_callbacks_,
+ remaining_ranges_[0].begin(), remaining_ranges_[0].end(), *response_headers);
+ }
+
+ decoder_callbacks_->encodeHeaders(std::move(response_headers), end_stream,
+ responseCodeDetailsFromStatus(lookup_result_->status_));
+ // onDestroy can potentially be called during encodeHeaders.
+ if (is_destroyed_) {
return;
- case CacheEntryStatus::LookupError:
- filter_state_ = FilterState::NotServingFromCache;
- insert_status_ = InsertStatus::NoInsertLookupError;
- decoder_callbacks_->continueDecoding();
+ }
+ if (end_stream) {
return;
}
- ENVOY_LOG(error, "Unhandled CacheEntryStatus in CacheFilter::onHeaders: {}",
- cacheEntryStatusString(cache_entry_status_.value()));
- // Treat unhandled status as a cache miss.
- sendUpstreamRequest(request_headers);
+ return getBody();
}
-// TODO(toddmgreer): Handle downstream backpressure.
-void CacheFilter::onBody(Buffer::InstancePtr&& body, bool end_stream) {
- // Can be called during decoding if a valid cache hit is found,
- // or during encoding if a cache entry was being validated.
- if (filter_state_ == FilterState::Destroyed) {
- // The filter is being destroyed, any callbacks should be ignored.
- return;
- }
+bool CacheFilter::onBody(Buffer::InstancePtr&& body, EndStream end_stream_enum) {
ASSERT(!remaining_ranges_.empty(),
"CacheFilter doesn't call getBody unless there's more body to get, so this is a "
"bogus callback.");
- if (remaining_ranges_[0].end() == std::numeric_limits::max() && body == nullptr) {
- ASSERT(!end_stream);
- getTrailers();
- return;
+ if (end_stream_enum == EndStream::Reset) {
+ decoder_callbacks_->streamInfo().setResponseCodeDetails(
+ CacheResponseCodeDetails::CacheFilterAbortedDuringBody);
+ decoder_callbacks_->resetStream();
+ return false;
+ }
+ bool end_stream = end_stream_enum == EndStream::End;
+
+ if (body == nullptr) {
+ // if we called getBody and got a nullptr that implies there was less body
+ // than expected, or we didn't have complete expectations.
+ // It should not be treated as a bug here to have incorrect expectations,
+ // as an untrusted upstream could send mismatched content-length and
+ // body-stream.
+ // If there is no body but there are trailers, this is how we know to
+ // move on to trailers.
+ if (end_stream) {
+ Buffer::OwnedImpl empty_buffer;
+ decoder_callbacks_->encodeData(empty_buffer, true);
+ finalizeEncodingCachedResponse();
+ return false;
+ } else {
+ getTrailers();
+ return false;
+ }
}
- 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_ranges_[0].length()) {
@@ -365,194 +409,79 @@ void CacheFilter::onBody(Buffer::InstancePtr&& body, bool end_stream) {
} else if (bytes_from_cache == remaining_ranges_[0].length()) {
remaining_ranges_.erase(remaining_ranges_.begin());
} else {
- ASSERT(false, "Received oversized body from cache.");
decoder_callbacks_->resetStream();
- return;
+ IS_ENVOY_BUG("Received oversized body from http source.");
+ return false;
+ }
+
+ // For a range request the upstream may not have thought it was end_stream
+ // but it still could be for the downstream.
+ if (is_partial_response_ && remaining_ranges_.empty()) {
+ end_stream = true;
}
decoder_callbacks_->encodeData(*body, end_stream);
+ // Filter can potentially be destroyed during encodeData (e.g. if
+ // encodeData provokes a reset)
+ if (is_destroyed_) {
+ return false;
+ }
if (end_stream) {
finalizeEncodingCachedResponse();
+ return false;
} else if (!remaining_ranges_.empty()) {
- getBody();
- } else if (lookup_result_->range_details_.has_value()) {
+ if (downstream_watermarked_) {
+ get_body_on_unblocked_ = true;
+ return false;
+ } else {
+ return true;
+ }
+ } else if (is_partial_response_) {
// If a range was requested we don't send trailers.
// (It is unclear from the spec whether we should, but pragmatically we
// don't have any indication of whether trailers are present or not, and
// range requests in general are for filling in missing chunks so including
// trailers with every chunk would be wasteful.)
finalizeEncodingCachedResponse();
+ return false;
} else {
getTrailers();
+ return false;
}
}
-void CacheFilter::onTrailers(Http::ResponseTrailerMapPtr&& trailers) {
- // Can be called during decoding if a valid cache hit is found,
- // or during encoding if a cache entry was being validated.
- if (filter_state_ == FilterState::Destroyed) {
- // The filter is being destroyed, any callbacks should be ignored.
- return;
- }
- decoder_callbacks_->encodeTrailers(std::move(trailers));
- // Filter can potentially be destroyed during encodeTrailers.
- if (filter_state_ == FilterState::Destroyed) {
- return;
- }
- finalizeEncodingCachedResponse();
-}
-
-void CacheFilter::handleCacheHit(bool end_stream_after_headers) {
- filter_state_ = FilterState::ServingFromCache;
- insert_status_ = InsertStatus::NoInsertCacheHit;
- encodeCachedResponse(end_stream_after_headers);
-}
+void CacheFilter::onAboveWriteBufferHighWatermark() { downstream_watermarked_++; }
-void CacheFilter::handleCacheHitWithRangeRequest() {
- if (!lookup_result_->range_details_.has_value()) {
- ENVOY_LOG(error, "handleCacheHitWithRangeRequest() should not be called without "
- "range_details_ being populated in lookup_result_");
- return;
- }
- if (!lookup_result_->range_details_->satisfiable_) {
- filter_state_ = FilterState::ServingFromCache;
- insert_status_ = InsertStatus::NoInsertCacheHit;
- lookup_result_->headers_->setStatus(
- static_cast(Envoy::Http::Code::RangeNotSatisfiable));
- if (lookup_result_->content_length_.has_value()) {
- lookup_result_->headers_->addCopy(
- Envoy::Http::Headers::get().ContentRange,
- absl::StrCat("bytes */", lookup_result_->content_length_.value()));
- } else {
- IS_ENVOY_BUG(
- "handleCacheHitWithRangeRequest() should not be called with satisfiable_=false "
- "without content_length_ being populated in lookup_result_. Cache implementation "
- "should wait to respond to getHeaders in this case until content_length_ is known, "
- "declaring a miss, or should strip range_details_ from the lookup result.");
- }
- // We shouldn't serve any of the body, so the response content length
- // is 0.
- lookup_result_->setContentLength(0);
- encodeCachedResponse(/* end_stream_after_headers = */ true);
- return;
- }
-
- std::vector ranges = lookup_result_->range_details_->ranges_;
- if (ranges.size() != 1) {
- // Multi-part responses are not supported, and they will be treated as
- // a usual 200 response. A possible way to achieve that would be to move
- // all ranges to remaining_ranges_, and add logic inside '::onBody' to
- // interleave the body bytes with sub-headers and separator string for
- // each part. Would need to keep track if the current range is over or
- // not to know when to insert the separator, and calculate the length
- // based on length of ranges + extra headers and separators.
- handleCacheHit(/* end_stream_after_headers = */ false);
- return;
- }
-
- filter_state_ = FilterState::ServingFromCache;
- insert_status_ = InsertStatus::NoInsertCacheHit;
-
- lookup_result_->headers_->setStatus(static_cast(Envoy::Http::Code::PartialContent));
- lookup_result_->headers_->addCopy(
- Envoy::Http::Headers::get().ContentRange,
- absl::StrCat("bytes ", ranges[0].begin(), "-", ranges[0].end() - 1, "/",
- lookup_result_->content_length_.has_value()
- ? absl::StrCat(lookup_result_->content_length_.value())
- : "*"));
- // We serve only the desired range, so adjust the length
- // accordingly.
- lookup_result_->setContentLength(ranges[0].length());
- remaining_ranges_ = std::move(ranges);
- encodeCachedResponse(/* end_stream_after_headers = */ false);
-}
-
-void CacheFilter::handleCacheHitWithValidation(Envoy::Http::RequestHeaderMap& request_headers) {
- filter_state_ = FilterState::ValidatingCachedResponse;
- injectValidationHeaders(request_headers);
- sendUpstreamRequest(request_headers);
-}
-
-void CacheFilter::injectValidationHeaders(Http::RequestHeaderMap& request_headers) {
- ASSERT(lookup_result_, "injectValidationHeaders precondition unsatisfied: lookup_result_ "
- "does not point to a cache lookup result");
- ASSERT(filter_state_ == FilterState::ValidatingCachedResponse,
- "injectValidationHeaders precondition unsatisfied: the "
- "CacheFilter is not validating a cache lookup result");
-
- const Http::HeaderEntry* etag_header =
- lookup_result_->headers_->getInline(CacheCustomHeaders::etag());
- const Http::HeaderEntry* last_modified_header =
- lookup_result_->headers_->getInline(CacheCustomHeaders::lastModified());
-
- if (etag_header) {
- absl::string_view etag = etag_header->value().getStringView();
- request_headers.setInline(CacheCustomHeaders::ifNoneMatch(), etag);
- }
- if (DateUtil::timePointValid(CacheHeadersUtils::httpTime(last_modified_header))) {
- // Valid Last-Modified header exists.
- absl::string_view last_modified = last_modified_header->value().getStringView();
- request_headers.setInline(CacheCustomHeaders::ifModifiedSince(), last_modified);
+void CacheFilter::onBelowWriteBufferLowWatermark() {
+ if (downstream_watermarked_ == 0) {
+ IS_ENVOY_BUG("low watermark not preceded by high watermark should not happen");
} else {
- // Either Last-Modified is missing or invalid, fallback to Date.
- // A correct behaviour according to:
- // https://httpwg.org/specs/rfc7232.html#header.if-modified-since
- absl::string_view date = lookup_result_->headers_->getDateValue();
- request_headers.setInline(CacheCustomHeaders::ifModifiedSince(), date);
+ downstream_watermarked_--;
+ }
+ if (downstream_watermarked_ == 0 && get_body_on_unblocked_) {
+ get_body_on_unblocked_ = false;
+ getBody();
}
}
-void CacheFilter::encodeCachedResponse(bool end_stream_after_headers) {
- ASSERT(lookup_result_, "encodeCachedResponse precondition unsatisfied: lookup_result_ "
- "does not point to a cache lookup result");
-
- // Set appropriate response flags and codes.
- decoder_callbacks_->streamInfo().setResponseFlag(
- StreamInfo::CoreResponseFlag::ResponseFromCacheFilter);
- decoder_callbacks_->streamInfo().setResponseCodeDetails(
- CacheResponseCodeDetails::get().ResponseFromCacheFilter);
-
- decoder_callbacks_->encodeHeaders(std::move(lookup_result_->headers_),
- is_head_request_ || end_stream_after_headers,
- CacheResponseCodeDetails::get().ResponseFromCacheFilter);
- // Filter can potentially be destroyed during encodeHeaders.
- if (filter_state_ == FilterState::Destroyed) {
+void CacheFilter::onTrailers(Http::ResponseTrailerMapPtr&& trailers, EndStream end_stream_enum) {
+ ASSERT(!is_destroyed_, "callback should be cancelled when filter is destroyed");
+ if (end_stream_enum == EndStream::Reset) {
+ decoder_callbacks_->streamInfo().setResponseCodeDetails(
+ CacheResponseCodeDetails::CacheFilterAbortedDuringTrailers);
+ decoder_callbacks_->resetStream();
return;
}
- if (is_head_request_ || end_stream_after_headers) {
- filter_state_ = FilterState::ResponseServedFromCache;
+ decoder_callbacks_->encodeTrailers(std::move(trailers));
+ // Filter can potentially be destroyed during encodeTrailers.
+ if (is_destroyed_) {
return;
}
- if (remaining_ranges_.empty() && lookup_result_->content_length_.value_or(1) > 0) {
- // No range has been added, so we add entire body to the response.
- remaining_ranges_.emplace_back(
- 0, lookup_result_->content_length_.value_or(std::numeric_limits::max()));
- }
- if (!remaining_ranges_.empty()) {
- getBody();
- } else {
- getTrailers();
- }
-}
-
-void CacheFilter::finalizeEncodingCachedResponse() {
- filter_state_ = FilterState::ResponseServedFromCache;
-}
-
-LookupStatus CacheFilter::lookupStatus() const {
- if (lookup_result_ == nullptr && lookup_ != nullptr) {
- return LookupStatus::RequestIncomplete;
- }
-
- return resolveLookupStatus(cache_entry_status_, filter_state_);
+ finalizeEncodingCachedResponse();
}
-InsertStatus CacheFilter::insertStatus() const {
- return insert_status_.value_or((upstream_request_ == nullptr)
- ? InsertStatus::NoInsertRequestIncomplete
- : InsertStatus::FilterAbortedBeforeInsertComplete);
-}
+void CacheFilter::finalizeEncodingCachedResponse() {}
} // namespace Cache
} // namespace HttpFilters
diff --git a/source/extensions/filters/http/cache/cache_filter.h b/source/extensions/filters/http/cache/cache_filter.h
index 3669bbf824c30..2be8ec73f512c 100644
--- a/source/extensions/filters/http/cache/cache_filter.h
+++ b/source/extensions/filters/http/cache/cache_filter.h
@@ -5,11 +5,11 @@
#include "envoy/extensions/filters/http/cache/v3/cache.pb.h"
+#include "source/common/common/cancel_wrapper.h"
#include "source/common/common/logger.h"
-#include "source/extensions/filters/http/cache/cache_filter_logging_info.h"
#include "source/extensions/filters/http/cache/cache_headers_utils.h"
-#include "source/extensions/filters/http/cache/filter_state.h"
-#include "source/extensions/filters/http/cache/http_cache.h"
+#include "source/extensions/filters/http/cache/cache_sessions.h"
+#include "source/extensions/filters/http/cache/stats.h"
#include "source/extensions/filters/http/common/pass_through_filter.h"
namespace Envoy {
@@ -17,19 +17,26 @@ namespace Extensions {
namespace HttpFilters {
namespace Cache {
-class UpstreamRequest;
-
-class CacheFilterConfig {
+// CacheFilterConfig contains everything which is shared by all CacheFilter
+// objects created from a given CacheConfig.
+class CacheFilterConfig : public CacheableResponseChecker, public CacheFilterStatsProvider {
public:
CacheFilterConfig(const envoy::extensions::filters::http::cache::v3::CacheConfig& config,
+ std::shared_ptr cache_sessions,
Server::Configuration::CommonFactoryContext& context);
+ // Implements CacheableResponseChecker::isCacheableResponse.
+ bool isCacheableResponse(const Http::ResponseHeaderMap& headers) const override;
// The allow list rules that decide if a header can be varied upon.
const VaryAllowList& varyAllowList() const { return vary_allow_list_; }
TimeSource& timeSource() const { return time_source_; }
const Http::AsyncClient::StreamOptions& upstreamOptions() const { return upstream_options_; }
Upstream::ClusterManager& clusterManager() const { return cluster_manager_; }
+ const std::string& overrideUpstreamCluster() const { return override_upstream_cluster_; }
bool ignoreRequestCacheControlHeader() const { return ignore_request_cache_control_header_; }
+ CacheSessions& cacheSessions() const { return *cache_sessions_; }
+ bool hasCache() const { return cache_sessions_ != nullptr; }
+ CacheFilterStats& stats() const override { return cache_sessions_->stats(); }
private:
const VaryAllowList vary_allow_list_;
@@ -37,111 +44,65 @@ class CacheFilterConfig {
const bool ignore_request_cache_control_header_;
Upstream::ClusterManager& cluster_manager_;
Http::AsyncClient::StreamOptions upstream_options_;
+ std::shared_ptr cache_sessions_;
+ CacheFilterStatsPtr stats_;
+ std::string override_upstream_cluster_;
};
/**
* A filter that caches responses and attempts to satisfy requests from cache.
*/
class CacheFilter : public Http::PassThroughFilter,
- public Logger::Loggable,
- public std::enable_shared_from_this {
+ public Http::DownstreamWatermarkCallbacks,
+ public Logger::Loggable {
public:
- CacheFilter(std::shared_ptr config,
- std::shared_ptr http_cache);
+ CacheFilter(std::shared_ptr config);
// Http::StreamFilterBase
void onDestroy() override;
- void onStreamComplete() override;
// Http::StreamDecoderFilter
+ void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override;
Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers,
bool end_stream) override;
// Http::StreamEncoderFilter
Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers,
bool end_stream) override;
- static LookupStatus resolveLookupStatus(absl::optional cache_entry_status,
- FilterState filter_state);
+ // Http::DownstreamWatermarkCallbacks
+ void onAboveWriteBufferHighWatermark() override;
+ void onBelowWriteBufferLowWatermark() override;
private:
- // For a cache miss that may be cacheable, the upstream request is sent outside of the usual
- // filter chain so that the request can continue even if the downstream client disconnects.
- void sendUpstreamRequest(Http::RequestHeaderMap& request_headers);
-
- // In the event that there is no matching route when attempting to sendUpstreamRequest,
- // send a 404 locally.
+ using CancelFunction = CancelWrapper::CancelFunction;
+ // Gets the cluster name for the current route, if there is one.
+ absl::optional clusterName();
+ // Gets an AsyncClient for the given cluster, or nullopt if there is no upstream.
+ OptRef asyncClient(absl::string_view cluster_name);
+
+ // In the event that there is no matching route when attempting to fetch asyncClient,
+ // send a 404 local response.
void sendNoRouteResponse();
- // In the event that there is no available cluster when attempting to sendUpstreamRequest,
- // send a 503 locally.
+ // In the event that there is no available cluster when attempting to fetch asyncClient,
+ // send a 503 local response.
void sendNoClusterResponse(absl::string_view cluster_name);
- // Called by UpstreamRequest if it is reset before CacheFilter is destroyed.
- // CacheFilter must make no more calls to upstream_request_ once this has been called.
- void onUpstreamRequestReset();
-
- // Called by UpstreamRequest if it finishes without reset before CacheFilter is destroyed.
- // CacheFilter must make no more calls to upstream_request_ once this has been called.
- void onUpstreamRequestComplete();
-
// Utility functions; make any necessary checks and call the corresponding lookup_ functions
void getHeaders(Http::RequestHeaderMap& request_headers);
void getBody();
void getTrailers();
- // Callbacks for HttpCache to call when headers/body/trailers are ready.
- void onHeaders(LookupResult&& result, Http::RequestHeaderMap& request_headers, bool end_stream);
- void onBody(Buffer::InstancePtr&& body, bool end_stream);
- void onTrailers(Http::ResponseTrailerMapPtr&& trailers);
-
- // Set required state in the CacheFilter for handling a cache hit.
- void handleCacheHit(bool end_stream_after_headers);
-
- // Set up the required state in the CacheFilter for handling a range
- // request.
- void handleCacheHitWithRangeRequest();
-
- // Set required state in the CacheFilter for handling a cache hit when
- // validation is required.
- void handleCacheHitWithValidation(Envoy::Http::RequestHeaderMap& request_headers);
-
- // Precondition: lookup_result_ points to a cache lookup result that requires validation.
- // Should only be called during onHeaders as it modifies RequestHeaderMap.
- // Adds required conditional headers for cache validation to the request headers
- // according to the present cache lookup result headers.
- void injectValidationHeaders(Http::RequestHeaderMap& request_headers);
-
- // Precondition: lookup_result_ points to a fresh or validated cache look up result.
- // Adds a cache lookup result to the response encoding stream.
- // Can be called during decoding if a valid cache hit is found,
- // or during encoding if a cache entry was validated successfully.
- //
- // When validating, headers should be set to the merged values from the validation
- // response and the lookup_result_; if unset, the headers from the lookup_result_ are used.
- void encodeCachedResponse(bool end_stream_after_headers);
-
- // Precondition: finished adding a response from cache to the response encoding stream.
- // Updates filter_state_ and continues the encoding stream if necessary.
+ void onLookupResult(ActiveLookupResultPtr lookup_result);
+ void onHeaders(Http::ResponseHeaderMapPtr headers, EndStream end_stream);
+ // Returns true if getBody should be called again.
+ bool onBody(Buffer::InstancePtr&& body, EndStream end_stream);
+ void onTrailers(Http::ResponseTrailerMapPtr&& trailers, EndStream end_stream);
+ CacheFilterStats& stats() const { return config_->stats(); }
+
void finalizeEncodingCachedResponse();
- // The result of this request's cache lookup.
- LookupStatus lookupStatus() const;
-
- // The final status of the insert operation or header update, or decision not
- // to insert or update. If the request or insert is ongoing, assumes it's
- // being cancelled.
- InsertStatus insertStatus() const;
-
- // upstream_request_ belongs to the object itself, so that it can be disconnected
- // from the filter and still complete the cache-write in the event that the
- // downstream disconnects. The filter and the UpstreamRequest must communicate to
- // each other their separate destruction-triggers.
- // When CacheFilter is destroyed first it should call
- // upstream_request_->disconnectFilter()
- // and if upstream_request_ is destroyed first, it will call onUpstreamRequestReset.
- UpstreamRequest* upstream_request_ = nullptr;
std::shared_ptr cache_;
- LookupContextPtr lookup_;
- LookupResultPtr lookup_result_;
- absl::optional cache_entry_status_;
+ ActiveLookupResultPtr lookup_result_;
+ bool is_partial_response_ = false;
// 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
@@ -154,16 +115,19 @@ class CacheFilter : public Http::PassThroughFilter,
// https://httpwg.org/specs/rfc7234.html#response.cacheability
bool request_allows_inserts_ = false;
- FilterState filter_state_ = FilterState::Initial;
+ bool is_destroyed_ = false;
bool is_head_request_ = false;
- // This toggle is used to detect callbacks being called directly and not posted.
- bool callback_called_directly_ = false;
- // The status of the insert operation or header update, or decision not to insert or update.
- // If it's too early to determine the final status, this is empty.
- absl::optional insert_status_;
-
- friend class UpstreamRequest;
+ // If this is populated it should be called from onDestroy.
+ CancelFunction cancel_in_flight_callback_;
+
+ int downstream_watermarked_ = 0;
+ // To avoid a potential recursion stack-overflow, the onBody function
+ // does not call getBody again directly but instead returns true if
+ // we *should* call getBody again, allowing it to be a loop rather
+ // than recursion.
+ enum class GetBodyLoop { InCallback, Again, Idle } get_body_loop_;
+ bool get_body_on_unblocked_ = false;
};
using CacheFilterSharedPtr = std::shared_ptr;
diff --git a/source/extensions/filters/http/cache/cache_filter_logging_info.cc b/source/extensions/filters/http/cache/cache_filter_logging_info.cc
deleted file mode 100644
index 94a5b94eb279f..0000000000000
--- a/source/extensions/filters/http/cache/cache_filter_logging_info.cc
+++ /dev/null
@@ -1,75 +0,0 @@
-#include "source/extensions/filters/http/cache/cache_filter_logging_info.h"
-
-#include "absl/strings/str_format.h"
-
-namespace Envoy {
-namespace Extensions {
-namespace HttpFilters {
-namespace Cache {
-
-absl::string_view lookupStatusToString(LookupStatus status) {
- switch (status) {
- case LookupStatus::Unknown:
- return "Unknown";
- case LookupStatus::CacheHit:
- return "CacheHit";
- case LookupStatus::CacheMiss:
- return "CacheMiss";
- case LookupStatus::StaleHitWithSuccessfulValidation:
- return "StaleHitWithSuccessfulValidation";
- case LookupStatus::StaleHitWithFailedValidation:
- return "StaleHitWithFailedValidation";
- case LookupStatus::NotModifiedHit:
- return "NotModifiedHit";
- case LookupStatus::RequestNotCacheable:
- return "RequestNotCacheable";
- case LookupStatus::RequestIncomplete:
- return "RequestIncomplete";
- case LookupStatus::LookupError:
- return "LookupError";
- }
- IS_ENVOY_BUG(absl::StrCat("Unexpected LookupStatus: ", status));
- return "UnexpectedLookupStatus";
-}
-
-std::ostream& operator<<(std::ostream& os, const LookupStatus& request_cache_status) {
- return os << lookupStatusToString(request_cache_status);
-}
-
-absl::string_view insertStatusToString(InsertStatus status) {
- switch (status) {
- case InsertStatus::InsertSucceeded:
- return "InsertSucceeded";
- case InsertStatus::InsertAbortedByCache:
- return "InsertAbortedByCache";
- case InsertStatus::InsertAbortedCacheCongested:
- return "InsertAbortedCacheCongested";
- case InsertStatus::FilterAbortedBeforeInsertComplete:
- return "FilterAbortedBeforeInsertComplete";
- case InsertStatus::HeaderUpdate:
- return "HeaderUpdate";
- case InsertStatus::NoInsertCacheHit:
- return "NoInsertCacheHit";
- case InsertStatus::NoInsertRequestNotCacheable:
- return "NoInsertRequestNotCacheable";
- case InsertStatus::NoInsertResponseNotCacheable:
- return "NoInsertResponseNotCacheable";
- case InsertStatus::NoInsertRequestIncomplete:
- return "NoInsertRequestIncomplete";
- case InsertStatus::NoInsertResponseValidatorsMismatch:
- return "NoInsertResponseValidatorsMismatch";
- case InsertStatus::NoInsertResponseVaryMismatch:
- return "NoInsertResponseVaryMismatch";
- case InsertStatus::NoInsertResponseVaryDisallowed:
- return "NoInsertResponseVaryDisallowed";
- case InsertStatus::NoInsertLookupError:
- return "NoInsertLookupError";
- }
- IS_ENVOY_BUG(absl::StrCat("Unexpected InsertStatus: ", status));
- return "UnexpectedInsertStatus";
-}
-
-} // namespace Cache
-} // namespace HttpFilters
-} // namespace Extensions
-} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_filter_logging_info.h b/source/extensions/filters/http/cache/cache_filter_logging_info.h
deleted file mode 100644
index 296fcf5d45f4f..0000000000000
--- a/source/extensions/filters/http/cache/cache_filter_logging_info.h
+++ /dev/null
@@ -1,112 +0,0 @@
-#pragma once
-
-#include "envoy/stream_info/filter_state.h"
-
-#include "absl/strings/str_format.h"
-
-namespace Envoy {
-namespace Extensions {
-namespace HttpFilters {
-namespace Cache {
-
-enum class LookupStatus {
- // The CacheFilter couldn't determine the status of the request, probably
- // because of an internal error.
- Unknown,
- // The CacheFilter found a response in cache to serve.
- CacheHit,
- // The CacheFilter didn't find a response in cache.
- CacheMiss,
- // The CacheFilter found a stale response, and sent a validation request to
- // the upstream; the upstream responded with a 304 Not Modified. This is
- // functionally a cache hit. It is differentiated for metrics reporting.
- StaleHitWithSuccessfulValidation,
- // The CacheFilter found a stale response, and sent a validation request to
- // the upstream; the upstream responded with anything other than a 304 Not
- // Modified. The CacheFilter forwards 5xx responses from the
- // upstream in this case, instead of sending the stale cache entry.
- StaleHitWithFailedValidation,
- // The CacheFilter found a response in cache and served a 304 Not Modified.
- NotModifiedHit,
- // The request wasn't cacheable, and the CacheFilter didn't try to look it up
- // in cache.
- RequestNotCacheable,
- // The request was cancelled before the CacheFilter could determine a cache
- // status.
- RequestIncomplete,
- // The CacheFilter couldn't determine whether there was a response in cache,
- // e.g. because the cache was unreachable or the lookup RPC timed out.
- LookupError,
-};
-
-absl::string_view lookupStatusToString(LookupStatus status);
-
-std::ostream& operator<<(std::ostream& os, const LookupStatus& request_cache_status);
-
-enum class InsertStatus {
- // The CacheFilter attempted to insert a cache entry, and succeeded as far as
- // it knows. The filter doesn't wait for a final confirmation from the cache,
- // so the filter may still show this status for an insert that failed at e.g.
- // the last body chunk.
- InsertSucceeded,
- // The CacheFilter started an insert, but the HttpCache aborted it.
- InsertAbortedByCache,
- // The CacheFilter started an insert, but aborted it because the cache wasn't
- // ready as a body chunk came in.
- InsertAbortedCacheCongested,
- // The CacheFilter started an insert, but the filter was reset before the insert
- // completed. The insert may or may not have gone on to completion independently.
- FilterAbortedBeforeInsertComplete,
- // The CacheFilter attempted to update the headers of an existing cache entry.
- // This doesn't indicate whether or not the update succeeded.
- HeaderUpdate,
- // The CacheFilter found a cache entry and didn't attempt to insert or update its
- // headers.
- NoInsertCacheHit,
- // The CacheFilter got an uncacheable request and didn't try to cache the
- // response.
- NoInsertRequestNotCacheable,
- // The CacheFilter got an uncacheable response and didn't cache it.
- NoInsertResponseNotCacheable,
- // The request was cancelled before the CacheFilter decided whether or not to
- // insert the response.
- NoInsertRequestIncomplete,
- // The CacheFilter got a 304 validation response not matching the etag strong
- // validator of our cached entry. The cached entry should be replaced or removed.
- NoInsertResponseValidatorsMismatch,
- // The CacheFilter got a 304 validation response not matching the vary header
- // fields. The cached variant set needs to be removed.
- NoInsertResponseVaryMismatch,
- // The CacheFilter got a 304 validation response, but the vary header was disallowed by the vary
- // allow list
- NoInsertResponseVaryDisallowed,
- // The CacheFilter couldn't determine whether the request was in cache and
- // didn't try to insert it.
- NoInsertLookupError,
-};
-
-absl::string_view insertStatusToString(InsertStatus status);
-
-// Cache-related information about a request, to be used for logging and stats.
-class CacheFilterLoggingInfo : public Envoy::StreamInfo::FilterState::Object {
-public:
- // FilterStateKey is used to store the FilterState::Object in the FilterState.
- static constexpr absl::string_view FilterStateKey =
- "io.envoyproxy.extensions.filters.http.cache.CacheFilterLoggingInfo";
-
- CacheFilterLoggingInfo(LookupStatus cache_lookup_status, InsertStatus cache_insert_status)
- : cache_lookup_status_(cache_lookup_status), cache_insert_status_(cache_insert_status) {}
-
- LookupStatus lookupStatus() const { return cache_lookup_status_; }
-
- InsertStatus insertStatus() const { return cache_insert_status_; }
-
-private:
- const LookupStatus cache_lookup_status_;
- const InsertStatus cache_insert_status_;
-};
-
-} // namespace Cache
-} // namespace HttpFilters
-} // namespace Extensions
-} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_headers_utils.cc b/source/extensions/filters/http/cache/cache_headers_utils.cc
index 06fd13bf898d2..d3bf53fc62d50 100644
--- a/source/extensions/filters/http/cache/cache_headers_utils.cc
+++ b/source/extensions/filters/http/cache/cache_headers_utils.cc
@@ -7,8 +7,10 @@
#include "envoy/http/header_map.h"
+#include "source/common/common/enum_to_int.h"
#include "source/common/http/header_map_impl.h"
#include "source/common/http/header_utility.h"
+#include "source/common/http/utility.h"
#include "source/extensions/filters/http/cache/cache_custom_headers.h"
#include "absl/algorithm/container.h"
@@ -232,6 +234,67 @@ Seconds CacheHeadersUtils::calculateAge(const Http::ResponseHeaderMap& response_
return std::chrono::duration_cast(current_age);
}
+void CacheHeadersUtils::injectValidationHeaders(
+ Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& old_response_headers) {
+ const Http::HeaderEntry* etag_header = old_response_headers.getInline(CacheCustomHeaders::etag());
+ const Http::HeaderEntry* last_modified_header =
+ old_response_headers.getInline(CacheCustomHeaders::lastModified());
+
+ if (etag_header) {
+ absl::string_view etag = etag_header->value().getStringView();
+ request_headers.setInline(CacheCustomHeaders::ifNoneMatch(), etag);
+ }
+ if (DateUtil::timePointValid(CacheHeadersUtils::httpTime(last_modified_header))) {
+ // Valid Last-Modified header exists.
+ absl::string_view last_modified = last_modified_header->value().getStringView();
+ request_headers.setInline(CacheCustomHeaders::ifModifiedSince(), last_modified);
+ } else {
+ // Either Last-Modified is missing or invalid, fallback to Date.
+ // A correct behaviour according to:
+ // https://httpwg.org/specs/rfc7232.html#header.if-modified-since
+ absl::string_view date = old_response_headers.getDateValue();
+ request_headers.setInline(CacheCustomHeaders::ifModifiedSince(), date);
+ }
+}
+
+// TODO(yosrym93): Write a test that exercises this when SimpleHttpCache implements updateHeaders
+bool CacheHeadersUtils::shouldUpdateCachedEntry(const Http::ResponseHeaderMap& new_headers,
+ const Http::ResponseHeaderMap& old_headers) {
+ ASSERT(Http::Utility::getResponseStatus(new_headers) == enumToInt(Http::Code::NotModified),
+ "shouldUpdateCachedEntry must only be called with 304 responses");
+
+ // According to: https://httpwg.org/specs/rfc7234.html#freshening.responses,
+ // and assuming a single cached response per key:
+ // If the 304 response contains a strong validator (etag) that does not match the cached response,
+ // the cached response should not be updated.
+ const Http::HeaderEntry* response_etag = new_headers.getInline(CacheCustomHeaders::etag());
+ const Http::HeaderEntry* cached_etag = old_headers.getInline(CacheCustomHeaders::etag());
+ return !response_etag || (cached_etag && cached_etag->value().getStringView() ==
+ response_etag->value().getStringView());
+}
+
+Key CacheHeadersUtils::makeKey(const Http::RequestHeaderMap& request_headers,
+ absl::string_view cluster_name) {
+ ASSERT(request_headers.Path(), "Can't form cache lookup key for malformed Http::RequestHeaderMap "
+ "with null Path.");
+ ASSERT(request_headers.Host(), "Can't form cache lookup key for malformed Http::RequestHeaderMap "
+ "with null Host.");
+ Key key;
+ absl::string_view scheme = request_headers.getSchemeValue();
+ ASSERT(Http::Utility::schemeIsValid(scheme));
+ // TODO(toddmgreer): Let config determine whether to include scheme, host, and
+ // query params.
+ key.set_cluster_name(cluster_name);
+ key.set_host(std::string(request_headers.getHostValue()));
+ key.set_path(std::string(request_headers.getPathValue()));
+ if (Http::Utility::schemeIsHttp(scheme)) {
+ key.set_scheme(Key::HTTP);
+ } else if (Http::Utility::schemeIsHttps(scheme)) {
+ key.set_scheme(Key::HTTPS);
+ }
+ return key;
+}
+
absl::optional CacheHeadersUtils::readAndRemoveLeadingDigits(absl::string_view& str) {
uint64_t val = 0;
uint32_t bytes_consumed = 0;
diff --git a/source/extensions/filters/http/cache/cache_headers_utils.h b/source/extensions/filters/http/cache/cache_headers_utils.h
index 5f96d24c54e22..b439cb9d9c636 100644
--- a/source/extensions/filters/http/cache/cache_headers_utils.h
+++ b/source/extensions/filters/http/cache/cache_headers_utils.h
@@ -11,6 +11,7 @@
#include "source/common/http/header_utility.h"
#include "source/common/http/headers.h"
#include "source/common/protobuf/protobuf.h"
+#include "source/extensions/filters/http/cache/key.pb.h"
#include "absl/container/btree_set.h"
#include "absl/strings/str_join.h"
@@ -107,6 +108,18 @@ SystemTime httpTime(const Http::HeaderEntry* header_entry);
Seconds calculateAge(const Http::ResponseHeaderMap& response_headers, SystemTime response_time,
SystemTime now);
+// Create a resource key from headers and cluster name.
+Key makeKey(const Http::RequestHeaderMap& request_headers, absl::string_view cluster_name);
+
+// Adds required conditional headers for cache validation to the request headers
+// according to the previous response headers.
+void injectValidationHeaders(Http::RequestHeaderMap& request_headers,
+ const Http::ResponseHeaderMap& old_response_headers);
+
+// Checks if a cached entry should be updated with a 304 response.
+bool shouldUpdateCachedEntry(const Http::ResponseHeaderMap& new_headers,
+ const Http::ResponseHeaderMap& old_headers);
+
/**
* Read a leading positive decimal integer value and advance "*str" past the
* digits read. If overflow occurs, or no digits exist, return
@@ -124,6 +137,14 @@ void getAllMatchingHeaderNames(const Http::HeaderMap& headers,
std::vector parseCommaDelimitedHeader(const Http::HeaderMap::GetResult& entry);
} // namespace CacheHeadersUtils
+// Helper abstraction for a container that contains a VaryAllowList.
+class CacheableResponseChecker {
+public:
+ // Calls CacheabilityUtils::isCacheableResponse with the contained VaryAllowList.
+ virtual bool isCacheableResponse(const Http::ResponseHeaderMap& headers) const PURE;
+ virtual ~CacheableResponseChecker() = default;
+};
+
class VaryAllowList {
public:
// Parses the allow list from the Cache Config into the object's private allow_list_.
diff --git a/source/extensions/filters/http/cache/cache_insert_queue.cc b/source/extensions/filters/http/cache/cache_insert_queue.cc
deleted file mode 100644
index 47ed1b1ab92e4..0000000000000
--- a/source/extensions/filters/http/cache/cache_insert_queue.cc
+++ /dev/null
@@ -1,235 +0,0 @@
-#include "source/extensions/filters/http/cache/cache_insert_queue.h"
-
-#include "source/common/buffer/buffer_impl.h"
-
-namespace Envoy {
-namespace Extensions {
-namespace HttpFilters {
-namespace Cache {
-
-// Representation of a piece of data to be sent to a cache for writing.
-class CacheInsertFragment {
-public:
- // Sends a fragment to the cache.
- // on_complete is called when the cache completes the operation.
- virtual void
- send(InsertContext& context,
- absl::AnyInvocable on_complete) PURE;
-
- virtual ~CacheInsertFragment() = default;
-};
-
-// A CacheInsertFragment containing some amount of http response body data.
-// The size of a fragment is equal to the size of the buffer arriving at
-// CacheFilter::encodeData.
-class CacheInsertFragmentBody : public CacheInsertFragment {
-public:
- CacheInsertFragmentBody(const Buffer::Instance& buffer, bool end_stream)
- : buffer_(buffer), end_stream_(end_stream) {}
-
- void send(InsertContext& context,
- absl::AnyInvocable on_complete)
- override {
- size_t sz = buffer_.length();
- context.insertBody(
- std::move(buffer_),
- [cb = std::move(on_complete), end_stream = end_stream_, sz](bool cache_success) mutable {
- std::move(cb)(cache_success, end_stream, sz);
- },
- end_stream_);
- }
-
-private:
- Buffer::OwnedImpl buffer_;
- const bool end_stream_;
-};
-
-// A CacheInsertFragment containing the full trailers of the response.
-class CacheInsertFragmentTrailers : public CacheInsertFragment {
-public:
- explicit CacheInsertFragmentTrailers(const Http::ResponseTrailerMap& trailers)
- : trailers_(Http::ResponseTrailerMapImpl::create()) {
- Http::ResponseTrailerMapImpl::copyFrom(*trailers_, trailers);
- }
-
- void send(InsertContext& context,
- absl::AnyInvocable on_complete)
- override {
- // While zero isn't technically true for the size of trailers, it doesn't
- // matter at this point because watermarks after the stream is complete
- // aren't useful.
- context.insertTrailers(*trailers_, [cb = std::move(on_complete)](bool cache_success) mutable {
- std::move(cb)(cache_success, true, 0);
- });
- }
-
-private:
- std::unique_ptr trailers_;
-};
-
-CacheInsertQueue::CacheInsertQueue(std::shared_ptr cache,
- Http::StreamEncoderFilterCallbacks& encoder_callbacks,
- InsertContextPtr insert_context, InsertQueueCallbacks& callbacks)
- : dispatcher_(encoder_callbacks.dispatcher()), insert_context_(std::move(insert_context)),
- low_watermark_bytes_(encoder_callbacks.encoderBufferLimit() / 2),
- high_watermark_bytes_(encoder_callbacks.encoderBufferLimit()), callbacks_(callbacks),
- cache_(cache) {}
-
-void CacheInsertQueue::insertHeaders(const Http::ResponseHeaderMap& response_headers,
- const ResponseMetadata& metadata, bool end_stream) {
- end_stream_queued_ = end_stream;
- // While zero isn't technically true for the size of headers, headers are
- // typically excluded from the stream buffer limit.
- fragment_in_flight_ = true;
- insert_context_->insertHeaders(
- response_headers, metadata,
- [this, end_stream](bool cache_success) { onFragmentComplete(cache_success, end_stream, 0); },
- end_stream);
- // This requirement simplifies the cache implementation; most caches will have to
- // do asynchronous operations, and so will post anyway. It is an error to call continueDecoding
- // during decodeHeaders, and calling a callback inline *may* do that, therefore we
- // require the cache to post. A previous version performed a post here to guarantee
- // correct behavior, but that meant for async caches it would double-post - it makes
- // more sense to single-post when it may not be necessary (in the rarer case of a cache
- // not needing async action) than to double-post in the common async case.
- // This requirement may become unnecessary after some more iterations result in
- // continueDecoding no longer being a thing in this filter.
- ASSERT(fragment_in_flight_,
- "insertHeaders must post the callback to dispatcher, not just call it");
-}
-
-void CacheInsertQueue::insertBody(const Buffer::Instance& fragment, bool end_stream) {
- if (end_stream) {
- end_stream_queued_ = true;
- }
- if (fragment_in_flight_) {
- size_t sz = fragment.length();
- queue_size_bytes_ += sz;
- fragments_.push_back(std::make_unique(fragment, end_stream));
- if (!watermarked_ && queue_size_bytes_ > high_watermark_bytes_) {
- if (callbacks_.has_value()) {
- callbacks_->insertQueueOverHighWatermark();
- }
- watermarked_ = true;
- }
- } else {
- fragment_in_flight_ = true;
- insert_context_->insertBody(
- Buffer::OwnedImpl(fragment),
- [this, end_stream](bool cache_success) {
- onFragmentComplete(cache_success, end_stream, 0);
- },
- end_stream);
- ASSERT(fragment_in_flight_,
- "insertBody must post the callback to dispatcher, not just call it");
- }
-}
-
-void CacheInsertQueue::insertTrailers(const Http::ResponseTrailerMap& trailers) {
- end_stream_queued_ = true;
- if (fragment_in_flight_) {
- fragments_.push_back(std::make_unique(trailers));
- } else {
- fragment_in_flight_ = true;
- insert_context_->insertTrailers(
- trailers, [this](bool cache_success) { onFragmentComplete(cache_success, true, 0); });
- ASSERT(fragment_in_flight_,
- "insertTrailers must post the callback to dispatcher, not just call it");
- }
-}
-
-void CacheInsertQueue::onFragmentComplete(bool cache_success, bool end_stream, size_t sz) {
- ASSERT(dispatcher_.isThreadSafe());
- fragment_in_flight_ = false;
- if (aborting_) {
- // Parent filter was destroyed, so we can quit this operation.
- fragments_.clear();
- self_ownership_.reset();
- return;
- }
- ASSERT(queue_size_bytes_ >= sz, "queue can't be emptied by more than its size");
- queue_size_bytes_ -= sz;
- if (watermarked_ && queue_size_bytes_ <= low_watermark_bytes_) {
- if (callbacks_.has_value()) {
- callbacks_->insertQueueUnderLowWatermark();
- }
- watermarked_ = false;
- }
- if (!cache_success) {
- // canceled by cache; unwatermark if necessary, inform the filter if
- // it's still around, and delete the queue.
- if (watermarked_) {
- if (callbacks_.has_value()) {
- callbacks_->insertQueueUnderLowWatermark();
- }
- watermarked_ = false;
- }
- fragments_.clear();
- // Clearing self-ownership might provoke the destructor, so take a copy of the
- // abort callback to avoid reading from 'this' after it may be deleted.
- //
- // This complexity is necessary because if the queue *is not* currently
- // self-owned, it will be deleted during insertQueueAborted, so
- // clearing self_ownership_ second would be a write-after-destroy error.
- // If it *is* currently self-owned, then we must still call the callback if
- // any, but clearing self_ownership_ *first* would mean we got destroyed
- // so we would no longer have access to the callback.
- // Since destroying first *or* second can be an error, rearrange things
- // so that destroying first *is not* an error. :)
- auto callbacks = std::move(callbacks_);
- self_ownership_.reset();
- if (callbacks.has_value()) {
- callbacks->insertQueueAborted();
- }
- return;
- }
- if (end_stream) {
- ASSERT(fragments_.empty(), "ending a stream with the queue not empty is a bug");
- ASSERT(!watermarked_, "being over the high watermark when the queue is empty makes no sense");
- self_ownership_.reset();
- return;
- }
- if (!fragments_.empty()) {
- // If there's more in the queue, push the next fragment to the cache.
- auto fragment = std::move(fragments_.front());
- fragments_.pop_front();
- fragment_in_flight_ = true;
- fragment->send(*insert_context_, [this](bool cache_success, bool end_stream, size_t sz) {
- onFragmentComplete(cache_success, end_stream, sz);
- });
- }
-}
-
-void CacheInsertQueue::setSelfOwned(std::unique_ptr self) {
- // If we sent a high watermark event, this is our last chance to unset it on the
- // stream, so we'd better do so.
- if (watermarked_) {
- if (callbacks_.has_value()) {
- callbacks_->insertQueueUnderLowWatermark();
- }
- watermarked_ = false;
- }
- // Disable all the callbacks, they're going to have nowhere to go.
- callbacks_.reset();
- if (fragments_.empty() && !fragment_in_flight_) {
- // If the queue is already empty we can just let it be destroyed immediately.
- return;
- }
- if (!end_stream_queued_) {
- // If the queue can't be completed we can abort early but we need to wait for
- // any callback-in-flight to complete before destroying the queue.
- aborting_ = true;
- }
- self_ownership_ = std::move(self);
-}
-
-CacheInsertQueue::~CacheInsertQueue() {
- ASSERT(!watermarked_, "should not have a watermarked status when the queue is destroyed");
- ASSERT(fragments_.empty(), "queue should be empty by the time the destructor is run");
- insert_context_->onDestroy();
-}
-
-} // namespace Cache
-} // namespace HttpFilters
-} // namespace Extensions
-} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_insert_queue.h b/source/extensions/filters/http/cache/cache_insert_queue.h
deleted file mode 100644
index 52537ef82f003..0000000000000
--- a/source/extensions/filters/http/cache/cache_insert_queue.h
+++ /dev/null
@@ -1,89 +0,0 @@
-#pragma once
-
-#include
-#include
-
-#include "source/extensions/filters/http/cache/http_cache.h"
-
-namespace Envoy {
-namespace Extensions {
-namespace HttpFilters {
-namespace Cache {
-
-class InsertQueueCallbacks {
-public:
- virtual void insertQueueOverHighWatermark() PURE;
- virtual void insertQueueUnderLowWatermark() PURE;
- virtual void insertQueueAborted() PURE;
- virtual ~InsertQueueCallbacks() = default;
-};
-class CacheInsertFragment;
-
-// This queue acts as an intermediary between CacheFilter and the cache
-// implementation extension. Having a queue allows CacheFilter to stream at its
-// normal rate, while allowing a cache implementation to run asynchronously and
-// potentially at a slower rate, without having to implement its own buffer.
-//
-// If the queue contains more than the "high watermark" for the buffer
-// (encoder_callbacks.encoderBufferLimit()), then a high watermark event is
-// sent to the encoder, which may cause the filter to slow down, to allow the
-// cache implementation time to catch up and avoid buffering significantly
-// more data in memory than the configuration intends to allow. When this happens,
-// the queue must drain to half the encoderBufferLimit before a low watermark
-// event is sent to resume normal flow.
-//
-// From the cache implementation's perspective, the queue ensures that the cache
-// receives data one piece at a time - no more data will be delivered until the
-// cache implementation calls the provided callback indicating that it is ready
-// to receive more data.
-class CacheInsertQueue {
-public:
- CacheInsertQueue(std::shared_ptr cache,
- Http::StreamEncoderFilterCallbacks& encoder_callbacks,
- InsertContextPtr insert_context, InsertQueueCallbacks& callbacks);
- void insertHeaders(const Http::ResponseHeaderMap& response_headers,
- const ResponseMetadata& metadata, bool end_stream);
- void insertBody(const Buffer::Instance& fragment, bool end_stream);
- void insertTrailers(const Http::ResponseTrailerMap& trailers);
- void setSelfOwned(std::unique_ptr self);
- ~CacheInsertQueue();
-
-private:
- void onFragmentComplete(bool cache_success, bool end_stream, size_t sz);
-
- Event::Dispatcher& dispatcher_;
- const InsertContextPtr insert_context_;
- const size_t low_watermark_bytes_, high_watermark_bytes_;
- OptRef callbacks_;
- std::deque> fragments_;
- // Size of the data currently in the queue (including any fragment in flight).
- size_t queue_size_bytes_ = 0;
- // True when the high watermark has been exceeded and the low watermark
- // threshold has not been crossed since.
- bool watermarked_ = false;
- // True when the queue has sent a fragment to the cache implementation and has
- // not yet received a response.
- bool fragment_in_flight_ = false;
- // True if end_stream has been queued. If the queue gets handed ownership
- // of itself before the end is in sight then it might as well abort since
- // it's not going to get a complete entry.
- bool end_stream_queued_ = false;
- // If the filter was deleted while !end_stream_queued_, aborting_ is set to
- // true; when the next fragment completes (or cancels), the queue is destroyed.
- bool aborting_ = false;
- // When the filter is destroyed, it passes ownership of CacheInsertQueue
- // to itself, because CacheInsertQueue can outlive the filter. The queue
- // will remove its self-ownership (thereby deleting itself) upon
- // completion of its work.
- std::unique_ptr self_ownership_;
- // The queue needs to keep a copy of the cache alive; if only the filter
- // keeps the cache alive then it's possible for the filter config to be deleted
- // while a cache action is still in flight, which can cause the cache to be
- // deleted prematurely.
- std::shared_ptr cache_;
-};
-
-} // namespace Cache
-} // namespace HttpFilters
-} // namespace Extensions
-} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_progress_receiver.h b/source/extensions/filters/http/cache/cache_progress_receiver.h
new file mode 100644
index 0000000000000..a724f084b247d
--- /dev/null
+++ b/source/extensions/filters/http/cache/cache_progress_receiver.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "envoy/http/header_map.h"
+
+#include "source/extensions/filters/http/cache/range_utils.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+class CacheReader;
+
+class CacheProgressReceiver {
+public:
+ virtual void onHeadersInserted(std::unique_ptr cache_entry,
+ Http::ResponseHeaderMapPtr headers, bool end_stream) PURE;
+ virtual void onBodyInserted(AdjustedByteRange range, bool end_stream) PURE;
+ virtual void onTrailersInserted(Http::ResponseTrailerMapPtr trailers) PURE;
+ virtual void onInsertFailed(absl::Status status) PURE;
+ virtual ~CacheProgressReceiver() = default;
+};
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_sessions.cc b/source/extensions/filters/http/cache/cache_sessions.cc
new file mode 100644
index 0000000000000..a9243b43ef085
--- /dev/null
+++ b/source/extensions/filters/http/cache/cache_sessions.cc
@@ -0,0 +1,111 @@
+#include "source/extensions/filters/http/cache/cache_sessions.h"
+
+#include
+
+#include "source/common/http/utility.h"
+#include "source/extensions/filters/http/cache/cache_custom_headers.h"
+#include "source/extensions/filters/http/cache/cache_headers_utils.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+ActiveLookupRequest::ActiveLookupRequest(
+ const Http::RequestHeaderMap& request_headers,
+ UpstreamRequestFactoryPtr upstream_request_factory, absl::string_view cluster_name,
+ Event::Dispatcher& dispatcher, SystemTime timestamp,
+ const std::shared_ptr cacheable_response_checker,
+ const std::shared_ptr stats_provider,
+ bool ignore_request_cache_control_header)
+ : upstream_request_factory_(std::move(upstream_request_factory)), dispatcher_(dispatcher),
+ key_(CacheHeadersUtils::makeKey(request_headers, cluster_name)),
+ request_headers_(Http::createHeaderMap(request_headers)),
+ cacheable_response_checker_(std::move(cacheable_response_checker)),
+ stats_provider_(std::move(stats_provider)), timestamp_(timestamp) {
+ if (!ignore_request_cache_control_header) {
+ initializeRequestCacheControl(request_headers);
+ }
+}
+
+absl::optional> ActiveLookupRequest::parseRange() const {
+ auto range_header = RangeUtils::getRangeHeader(*request_headers_);
+ if (!range_header) {
+ return absl::nullopt;
+ }
+ return RangeUtils::parseRangeHeader(range_header.value(), 1);
+}
+
+bool ActiveLookupRequest::isRangeRequest() const {
+ return RangeUtils::getRangeHeader(*request_headers_).has_value();
+}
+
+void ActiveLookupRequest::initializeRequestCacheControl(
+ const Http::RequestHeaderMap& request_headers) {
+ const absl::string_view cache_control =
+ request_headers.getInlineValue(CacheCustomHeaders::requestCacheControl());
+
+ if (!cache_control.empty()) {
+ request_cache_control_ = RequestCacheControl(cache_control);
+ } else {
+ const absl::string_view pragma = request_headers.getInlineValue(CacheCustomHeaders::pragma());
+ // According to: https://httpwg.org/specs/rfc7234.html#header.pragma,
+ // when Cache-Control header is missing, "Pragma:no-cache" is equivalent to
+ // "Cache-Control:no-cache". Any other directives are ignored.
+ request_cache_control_.must_validate_ = RequestCacheControl(pragma).must_validate_;
+ }
+}
+
+bool ActiveLookupRequest::requiresValidation(const Http::ResponseHeaderMap& response_headers,
+ SystemTime::duration response_age) const {
+ // TODO(yosrym93): Store parsed response cache-control in cache instead of parsing it on every
+ // lookup.
+ const absl::string_view cache_control =
+ response_headers.getInlineValue(CacheCustomHeaders::responseCacheControl());
+ const ResponseCacheControl response_cache_control(cache_control);
+
+ const bool request_max_age_exceeded = request_cache_control_.max_age_.has_value() &&
+ request_cache_control_.max_age_.value() < response_age;
+ if (response_cache_control.must_validate_ || request_cache_control_.must_validate_ ||
+ request_max_age_exceeded) {
+ // Either the request or response explicitly require validation, or a request max-age
+ // requirement is not satisfied.
+ return true;
+ }
+
+ // CacheabilityUtils::isCacheableResponse(..) guarantees that any cached response satisfies this.
+ ASSERT(response_cache_control.max_age_.has_value() ||
+ (response_headers.getInline(CacheCustomHeaders::expires()) && response_headers.Date()),
+ "Cache entry does not have valid expiration data.");
+
+ SystemTime::duration freshness_lifetime;
+ if (response_cache_control.max_age_.has_value()) {
+ freshness_lifetime = response_cache_control.max_age_.value();
+ } else {
+ const SystemTime expires_value =
+ CacheHeadersUtils::httpTime(response_headers.getInline(CacheCustomHeaders::expires()));
+ const SystemTime date_value = CacheHeadersUtils::httpTime(response_headers.Date());
+ freshness_lifetime = expires_value - date_value;
+ }
+
+ if (response_age > freshness_lifetime) {
+ // Response is stale, requires validation if
+ // the response does not allow being served stale,
+ // or the request max-stale directive does not allow it.
+ const bool allowed_by_max_stale =
+ request_cache_control_.max_stale_.has_value() &&
+ request_cache_control_.max_stale_.value() > response_age - freshness_lifetime;
+ return response_cache_control.no_stale_ || !allowed_by_max_stale;
+ } else {
+ // Response is fresh, requires validation only if there is an unsatisfied min-fresh requirement.
+ const bool min_fresh_unsatisfied =
+ request_cache_control_.min_fresh_.has_value() &&
+ request_cache_control_.min_fresh_.value() > freshness_lifetime - response_age;
+ return min_fresh_unsatisfied;
+ }
+}
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_sessions.h b/source/extensions/filters/http/cache/cache_sessions.h
new file mode 100644
index 0000000000000..573e9efa023db
--- /dev/null
+++ b/source/extensions/filters/http/cache/cache_sessions.h
@@ -0,0 +1,105 @@
+#pragma once
+
+#include
+
+#include "envoy/buffer/buffer.h"
+
+#include "source/extensions/filters/http/cache/http_cache.h"
+#include "source/extensions/filters/http/cache/key.pb.h"
+#include "source/extensions/filters/http/cache/stats.h"
+#include "source/extensions/filters/http/cache/upstream_request.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+class ActiveLookupRequest {
+public:
+ // Prereq: request_headers's Path(), Scheme(), and Host() are non-null.
+ ActiveLookupRequest(
+ const Http::RequestHeaderMap& request_headers,
+ UpstreamRequestFactoryPtr upstream_request_factory, absl::string_view cluster_name,
+ Event::Dispatcher& dispatcher, SystemTime timestamp,
+ const std::shared_ptr cacheable_response_checker_,
+ const std::shared_ptr stats_provider_,
+ bool ignore_request_cache_control_header);
+
+ // Caches may modify the key according to local needs, though care must be
+ // taken to ensure that meaningfully distinct responses have distinct keys.
+ const Key& key() const { return key_; }
+
+ Http::RequestHeaderMap& requestHeaders() const { return *request_headers_; }
+ bool isCacheableResponse(const Http::ResponseHeaderMap& headers) const {
+ return cacheable_response_checker_->isCacheableResponse(headers);
+ }
+ const std::shared_ptr& cacheableResponseChecker() const {
+ return cacheable_response_checker_;
+ }
+ const std::shared_ptr& statsProvider() const {
+ return stats_provider_;
+ }
+ CacheFilterStats& stats() const { return statsProvider()->stats(); }
+ UpstreamRequestPtr createUpstreamRequest() const {
+ return upstream_request_factory_->create(statsProvider());
+ }
+ Event::Dispatcher& dispatcher() const { return dispatcher_; }
+ SystemTime timestamp() const { return timestamp_; }
+ bool requiresValidation(const Http::ResponseHeaderMap& response_headers,
+ SystemTime::duration age) const;
+ absl::optional> parseRange() const;
+ bool isRangeRequest() const;
+
+private:
+ void initializeRequestCacheControl(const Http::RequestHeaderMap& request_headers);
+
+ UpstreamRequestFactoryPtr upstream_request_factory_;
+ Event::Dispatcher& dispatcher_;
+ Key key_;
+ std::vector request_range_spec_;
+ Http::RequestHeaderMapPtr request_headers_;
+ const std::shared_ptr cacheable_response_checker_;
+ const std::shared_ptr stats_provider_;
+ // Time when this LookupRequest was created (in response to an HTTP request).
+ SystemTime timestamp_;
+ RequestCacheControl request_cache_control_;
+};
+using ActiveLookupRequestPtr = std::unique_ptr;
+
+struct ActiveLookupResult {
+ // The source from which headers, body and trailers can be retrieved. May be
+ // a cache-reader CacheSession, or may be an UpstreamRequest if the request
+ // was uncacheable. The filter doesn't need to know which.
+ std::unique_ptr http_source_;
+
+ CacheEntryStatus status_;
+};
+
+using ActiveLookupResultPtr = std::unique_ptr;
+using ActiveLookupResultCallback = absl::AnyInvocable;
+
+// CacheSessions is a wrapper around an HttpCache which provides a shorter-lived in-memory
+// cache of headers and already open cache entries. All the http-specific aspects of the
+// cache (range requests, validation, etc.) are performed by the CacheSession
+// so the HttpCache only needs to support simple read/write operations.
+//
+// May or may not be a singleton, depending on the specific cache extension; must include
+// the Singleton::Instance interface to support cases when it is.
+class CacheSessions : public Singleton::Instance, public CacheFilterStatsProvider {
+public:
+ // This is implemented in CacheSessionsImpl so that tests which only use a mock don't
+ // need to build the real thing, but declared here so that the actual use-site can
+ // create an instance without including the larger header.
+ static std::shared_ptr create(Server::Configuration::FactoryContext& context,
+ std::unique_ptr cache);
+
+ virtual void lookup(ActiveLookupRequestPtr request, ActiveLookupResultCallback&& cb) PURE;
+ virtual HttpCache& cache() const PURE;
+ CacheInfo cacheInfo() const { return cache().cacheInfo(); }
+ ~CacheSessions() override = default;
+};
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_sessions_impl.cc b/source/extensions/filters/http/cache/cache_sessions_impl.cc
new file mode 100644
index 0000000000000..6720184d64e29
--- /dev/null
+++ b/source/extensions/filters/http/cache/cache_sessions_impl.cc
@@ -0,0 +1,917 @@
+#include "source/extensions/filters/http/cache/cache_sessions_impl.h"
+
+#include "source/common/buffer/buffer_impl.h"
+#include "source/common/common/enum_to_int.h"
+#include "source/common/http/utility.h"
+#include "source/extensions/filters/http/cache/cache_custom_headers.h"
+#include "source/extensions/filters/http/cache/cache_entry_utils.h"
+#include "source/extensions/filters/http/cache/cache_headers_utils.h"
+#include "source/extensions/filters/http/cache/cacheability_utils.h"
+#include "source/extensions/filters/http/cache/range_utils.h"
+#include "source/extensions/filters/http/cache/upstream_request.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+using CancelWrapper::cancelWrapped;
+
+class UpstreamRequestWithCacheabilityReset : public HttpSource {
+public:
+ UpstreamRequestWithCacheabilityReset(
+ std::shared_ptr cacheable_response_checker,
+ std::unique_ptr original_source, std::shared_ptr entry)
+ : cacheable_response_checker_(cacheable_response_checker),
+ original_source_(std::move(original_source)), entry_(std::move(entry)) {}
+ void getHeaders(GetHeadersCallback&& cb) override {
+ original_source_->getHeaders(
+ [entry = std::move(entry_), cb = std::move(cb),
+ cacheable_response_checker = std::move(cacheable_response_checker_)](
+ Http::ResponseHeaderMapPtr headers, EndStream end_stream) mutable {
+ if (cacheable_response_checker->isCacheableResponse(*headers)) {
+ entry->clearUncacheableState();
+ }
+ cb(std::move(headers), end_stream);
+ });
+ }
+ void getBody(AdjustedByteRange range, GetBodyCallback&& cb) override {
+ original_source_->getBody(std::move(range), std::move(cb));
+ }
+ void getTrailers(GetTrailersCallback&& cb) override {
+ original_source_->getTrailers(std::move(cb));
+ }
+
+private:
+ std::shared_ptr cacheable_response_checker_;
+ std::unique_ptr original_source_;
+ std::shared_ptr entry_;
+};
+
+class UpstreamRequestWithHeadersPrepopulated : public HttpSource {
+public:
+ UpstreamRequestWithHeadersPrepopulated(std::unique_ptr original_source,
+ Http::ResponseHeaderMapPtr headers, EndStream end_stream)
+ : original_source_(std::move(original_source)), headers_(std::move(headers)),
+ end_stream_after_headers_(end_stream) {}
+ void getHeaders(GetHeadersCallback&& cb) override {
+ cb(std::move(headers_), end_stream_after_headers_);
+ }
+ void getBody(AdjustedByteRange range, GetBodyCallback&& cb) override {
+ original_source_->getBody(std::move(range), std::move(cb));
+ }
+ void getTrailers(GetTrailersCallback&& cb) override {
+ original_source_->getTrailers(std::move(cb));
+ }
+
+private:
+ std::unique_ptr original_source_;
+ Http::ResponseHeaderMapPtr headers_;
+ EndStream end_stream_after_headers_;
+};
+
+static Http::RequestHeaderMapPtr
+requestHeadersWithRangeRemoved(const Http::RequestHeaderMap& original_headers) {
+ Http::RequestHeaderMapPtr headers =
+ Http::createHeaderMap(original_headers);
+ headers->remove(Envoy::Http::Headers::get().Range);
+ return headers;
+}
+
+static Http::ResponseHeaderMapPtr notSatisfiableHeaders() {
+ static const std::string not_satisfiable =
+ std::to_string(enumToInt(Http::Code::RangeNotSatisfiable));
+ return Http::createHeaderMap({
+ {Http::Headers::get().Status, not_satisfiable},
+ {Http::Headers::get().ContentLength, "0"},
+ });
+}
+
+void ActiveLookupContext::getHeaders(GetHeadersCallback&& cb) {
+ absl::optional> ranges = lookup().parseRange();
+ if (ranges) {
+ // If it's a range request, inject the appropriate modified content-range and
+ // content-length headers into the response once we have the response headers.
+ entry_->wantHeaders(
+ dispatcher(), lookup().timestamp(),
+ [ranges = std::move(ranges.value()), cl = content_length_,
+ cb = std::move(cb)](Http::ResponseHeaderMapPtr headers, EndStream end_stream) mutable {
+ ASSERT(headers != nullptr, "it should be impossible for headers to be null");
+ if (cl == 0 && headers->ContentLength()) {
+ absl::SimpleAtoi(headers->getContentLengthValue(), &cl) || (cl = 0);
+ }
+ RangeDetails range_details = RangeUtils::createAdjustedRangeDetails(ranges, cl);
+ if (!range_details.satisfiable_) {
+ return cb(notSatisfiableHeaders(), EndStream::End);
+ }
+ if (range_details.ranges_.empty()) {
+ return cb(std::move(headers), end_stream);
+ }
+ auto& range = range_details.ranges_[0];
+ headers->setReferenceKey(
+ Envoy::Http::Headers::get().ContentRange,
+ fmt::format("bytes {}-{}/{}", range.begin(), range.end() - 1, cl));
+ headers->setContentLength(range.length());
+ static const std::string partial_content =
+ std::to_string(enumToInt(Http::Code::PartialContent));
+ headers->setStatus(partial_content);
+ cb(std::move(headers), end_stream);
+ });
+ } else {
+ entry_->wantHeaders(dispatcher(), lookup().timestamp(), std::move(cb));
+ }
+}
+
+void ActiveLookupContext::getBody(AdjustedByteRange range, GetBodyCallback&& cb) {
+ entry_->wantBodyRange(range, dispatcher(), std::move(cb));
+}
+
+void ActiveLookupContext::getTrailers(GetTrailersCallback&& cb) {
+ entry_->wantTrailers(dispatcher(), std::move(cb));
+}
+
+std::shared_ptr CacheSessions::create(Server::Configuration::FactoryContext& context,
+ std::unique_ptr cache) {
+ return std::make_shared(context, std::move(cache));
+}
+
+CacheSession::CacheSession(std::weak_ptr cache_sessions, const Key& key)
+ : cache_sessions_(std::move(cache_sessions)), key_(key) {}
+
+void CacheSession::clearUncacheableState() {
+ absl::MutexLock lock(&mu_);
+ if (state_ != State::NotCacheable) {
+ return;
+ }
+ state_ = State::New;
+}
+
+void CacheSession::wantHeaders(Event::Dispatcher&, SystemTime lookup_timestamp,
+ GetHeadersCallback&& cb) {
+ Http::ResponseHeaderMapPtr headers;
+ EndStream end_stream_after_headers;
+ {
+ absl::MutexLock lock(&mu_);
+ ASSERT(entry_.response_headers_ != nullptr,
+ "headers should have been initialized during lookup");
+ headers = Http::createHeaderMap(*entry_.response_headers_);
+ Seconds age = CacheHeadersUtils::calculateAge(
+ *headers, entry_.response_metadata_.response_time_, lookup_timestamp);
+ headers->setReferenceKey(Envoy::Http::CustomHeaders::get().Age, std::to_string(age.count()));
+ end_stream_after_headers = endStreamAfterHeaders();
+ }
+ cb(std::move(headers), end_stream_after_headers);
+}
+
+void CacheSession::wantBodyRange(AdjustedByteRange range, Event::Dispatcher& dispatcher,
+ GetBodyCallback&& cb) {
+ absl::MutexLock lock(&mu_);
+ ASSERT(entry_.response_headers_ != nullptr,
+ "body should not be requested when headers haven't been sent");
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().incCacheSessionsSubscribers();
+ }
+ body_subscribers_.emplace_back(dispatcher, std::move(range), std::move(cb));
+ // if there's not already a body read operation in flight, start one.
+ maybeTriggerBodyReadForWaitingSubscriber();
+}
+
+void CacheSession::wantTrailers(Event::Dispatcher& dispatcher, GetTrailersCallback&& cb) {
+ absl::MutexLock lock(&mu_);
+ if (entry_.response_trailers_ != nullptr) {
+ auto trailers = Http::createHeaderMap(*entry_.response_trailers_);
+ dispatcher.post([cb = std::move(cb), trailers = std::move(trailers)]() mutable {
+ cb(std::move(trailers), EndStream::End);
+ });
+ return;
+ }
+ ASSERT(!entry_.body_length_.has_value(),
+ "wantTrailers should not be called when there are no trailers");
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().incCacheSessionsSubscribers();
+ }
+ trailer_subscribers_.emplace_back(dispatcher, std::move(cb));
+}
+
+void CacheSession::onHeadersInserted(CacheReaderPtr cache_reader,
+ Http::ResponseHeaderMapPtr headers, bool end_stream) {
+ absl::MutexLock lock(&mu_);
+ std::shared_ptr cache_sessions = cache_sessions_.lock();
+ if (!cache_sessions) {
+ ENVOY_LOG(error, "cache config was deleted while header-insertion was in flight");
+ return onCacheWentAway();
+ }
+ entry_.cache_reader_ = std::move(cache_reader);
+ entry_.response_headers_ = std::move(headers);
+ entry_.response_metadata_ = cache_sessions->makeMetadata();
+ if (end_stream) {
+ insertComplete();
+ } else {
+ state_ = State::Inserting;
+ }
+ sendLookupResponsesAndMaybeValidationRequest(CacheEntryStatus::Miss);
+}
+
+bool CacheSession::requiresValidationFor(const ActiveLookupRequest& lookup) const {
+ mu_.AssertHeld();
+ const Seconds age = CacheHeadersUtils::calculateAge(
+ *entry_.response_headers_, entry_.response_metadata_.response_time_, lookup.timestamp());
+ return lookup.requiresValidation(*entry_.response_headers_, age);
+}
+
+void CacheSession::sendLookupResponsesAndMaybeValidationRequest(CacheEntryStatus status) {
+ mu_.AssertHeld();
+ ASSERT(state_ == State::Exists || state_ == State::Inserting);
+ auto it = lookup_subscribers_.begin();
+ if (status != CacheEntryStatus::Miss) {
+ // Reorder subscribers so those who do not require validation are at the end,
+ // and 'it' is the first subscriber that does not require validation.
+ it = std::partition(lookup_subscribers_.begin(), lookup_subscribers_.end(),
+ [this](LookupSubscriber& s) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) {
+ return requiresValidationFor(s.context_->lookup());
+ });
+ }
+ for (auto recipient = it; recipient != lookup_subscribers_.end(); recipient++) {
+ sendSuccessfulLookupResultTo(*recipient, status);
+ // If there was more than one recipient, and the first one was a miss, the
+ // rest will be streamed.
+ if (status == CacheEntryStatus::Miss) {
+ status = CacheEntryStatus::Follower;
+ }
+ }
+ if (it != lookup_subscribers_.end()) {
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().subCacheSessionsSubscribers(
+ std::distance(it, lookup_subscribers_.end()));
+ }
+ }
+ lookup_subscribers_.erase(it, lookup_subscribers_.end());
+ if (!lookup_subscribers_.empty()) {
+ // At least one subscriber required validation.
+ return performValidation();
+ }
+}
+
+EndStream CacheSession::endStreamAfterHeaders() const {
+ mu_.AssertHeld();
+ bool end_stream = entry_.body_length_.value_or(1) == 0 && entry_.response_trailers_ == nullptr;
+ return end_stream ? EndStream::End : EndStream::More;
+}
+
+EndStream CacheSession::endStreamAfterBody() const {
+ mu_.AssertHeld();
+ ASSERT(entry_.body_length_.has_value(),
+ "should not be testing endStreamAfterBody if body not complete");
+ return (entry_.response_trailers_ == nullptr) ? EndStream::End : EndStream::More;
+}
+
+void CacheSession::sendSuccessfulLookupResultTo(LookupSubscriber& subscriber,
+ CacheEntryStatus status) {
+ mu_.AssertHeld();
+ ASSERT(state_ == State::Exists || state_ == State::Inserting);
+ auto result = std::make_unique();
+ result->status_ = status;
+ result->http_source_ = std::move(subscriber.context_);
+ subscriber.dispatcher().post(
+ [result = std::move(result), callback = std::move(subscriber.callback_)]() mutable {
+ callback(std::move(result));
+ });
+}
+
+void CacheSession::onBodyInserted(AdjustedByteRange range, bool end_stream) {
+ absl::MutexLock lock(&mu_);
+ body_length_available_ = range.end();
+ if (end_stream) {
+ insertComplete();
+ ASSERT(trailer_subscribers_.empty(), "should not be trailer requests before body was complete");
+ }
+ maybeTriggerBodyReadForWaitingSubscriber();
+}
+
+void CacheSession::onTrailersInserted(Http::ResponseTrailerMapPtr trailers) {
+ ASSERT(trailers);
+ absl::MutexLock lock(&mu_);
+ entry_.response_trailers_ = std::move(trailers);
+ insertComplete();
+ for (TrailerSubscriber& subscriber : trailer_subscribers_) {
+ sendTrailersTo(subscriber);
+ }
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().subCacheSessionsSubscribers(trailer_subscribers_.size());
+ }
+ trailer_subscribers_.clear();
+ // If there's a body subscriber waiting for more body that doesn't exist,
+ // it needs to be notified so it can call getTrailers.
+ abortBodyOutOfRangeSubscribers();
+}
+
+void CacheSession::sendTrailersTo(TrailerSubscriber& subscriber) {
+ mu_.AssertHeld();
+ ASSERT(entry_.response_trailers_ != nullptr);
+ subscriber.dispatcher().post(
+ [trailers = Http::createHeaderMap(*entry_.response_trailers_),
+ callback = std::move(subscriber.callback_)]() mutable {
+ callback(std::move(trailers), EndStream::End);
+ });
+}
+
+void CacheSession::onInsertFailed(absl::Status status) {
+ absl::MutexLock lock(&mu_);
+ ENVOY_LOG(error, "cache insert failed: {}", status);
+ onCacheError();
+}
+
+static void postUpstreamPassThrough(CacheSession::LookupSubscriber&& sub, CacheEntryStatus status) {
+ Event::Dispatcher& dispatcher = sub.dispatcher();
+ dispatcher.post([sub = std::move(sub), status]() mutable {
+ auto result = std::make_unique();
+ auto upstream = sub.context_->lookup().createUpstreamRequest();
+ upstream->sendHeaders(
+ Http::createHeaderMap(sub.context_->lookup().requestHeaders()));
+ result->http_source_ = std::move(upstream);
+ result->status_ = status;
+ sub.callback_(std::move(result));
+ });
+}
+
+static void postUpstreamPassThroughWithReset(CacheSession::LookupSubscriber&& sub,
+ std::shared_ptr entry) {
+ Event::Dispatcher& dispatcher = sub.dispatcher();
+ dispatcher.post([sub = std::move(sub), entry = std::move(entry)]() mutable {
+ auto result = std::make_unique();
+ auto upstream = sub.context_->lookup().createUpstreamRequest();
+ upstream->sendHeaders(
+ Http::createHeaderMap(sub.context_->lookup().requestHeaders()));
+ result->http_source_ = std::make_unique(
+ sub.context_->lookup().cacheableResponseChecker(), std::move(upstream), entry);
+ result->status_ = CacheEntryStatus::Uncacheable;
+ sub.callback_(std::move(result));
+ });
+}
+
+void CacheSession::onCacheError() {
+ mu_.AssertHeld();
+ auto cache_sessions = cache_sessions_.lock();
+ if (cache_sessions) {
+ Event::Dispatcher* dispatcher = nullptr;
+ if (!lookup_subscribers_.empty()) {
+ dispatcher = &lookup_subscribers_.front().dispatcher();
+ } else if (!body_subscribers_.empty()) {
+ dispatcher = &body_subscribers_.front().dispatcher();
+ } else if (!trailer_subscribers_.empty()) {
+ dispatcher = &trailer_subscribers_.front().dispatcher();
+ }
+ if (dispatcher) {
+ // TODO(toddmgreer): there may be some kinds of cache error that
+ // don't merit evicting the entry.
+ cache_sessions->cache().evict(*dispatcher, key_);
+ }
+ cache_sessions->stats().subCacheSessionsSubscribers(body_subscribers_.size());
+ cache_sessions->stats().subCacheSessionsSubscribers(trailer_subscribers_.size());
+ cache_sessions->stats().subCacheSessionsSubscribers(lookup_subscribers_.size());
+ }
+ for (LookupSubscriber& sub : lookup_subscribers_) {
+ postUpstreamPassThrough(std::move(sub), CacheEntryStatus::LookupError);
+ }
+ for (BodySubscriber& sub : body_subscribers_) {
+ sub.callback_(nullptr, EndStream::Reset);
+ }
+ for (TrailerSubscriber& sub : trailer_subscribers_) {
+ sub.callback_(nullptr, EndStream::Reset);
+ }
+ lookup_subscribers_.clear();
+ body_subscribers_.clear();
+ trailer_subscribers_.clear();
+ state_ = State::New;
+}
+
+void CacheSession::insertComplete() {
+ mu_.AssertHeld();
+ state_ = State::Exists;
+ entry_.body_length_ = body_length_available_;
+ if (content_length_header_ == entry_.body_length_) {
+ return;
+ }
+ if (content_length_header_ != 0) {
+ ENVOY_LOG(error,
+ "cache insert for {}{} had content-length header {} but actual size {}. Cache has "
+ "modified the header to match actual size.",
+ key_.host(), key_.path(), content_length_header_, entry_.body_length_.value());
+ }
+ content_length_header_ = body_length_available_;
+}
+
+void CacheSession::abortBodyOutOfRangeSubscribers() {
+ mu_.AssertHeld();
+ if (!entry_.body_length_.has_value()) {
+ // Don't know if a request is out of range until the available range is known.
+ return;
+ }
+ // For any subscribers whose requested range has been revealed to be invalid
+ // (we only get here in the case where content length was specified in the
+ // headers, but the actual body was shorter, i.e. the upstream response was
+ // actually invalid), reset their requests.
+ // Subscribers who asked for body starting at or beyond the end of the
+ // real size receive null body rather than reset.
+ EndStream end_stream = endStreamAfterBody();
+ auto cache_sessions = cache_sessions_.lock();
+ body_subscribers_.erase(
+ std::remove_if(body_subscribers_.begin(), body_subscribers_.end(),
+ [this, end_stream, &cache_sessions](BodySubscriber& bs)
+ ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) {
+ if (bs.range_.begin() >= body_length_available_) {
+ if (bs.range_.begin() == body_length_available_) {
+ auto cb = std::move(bs.callback_);
+ bs.dispatcher().post([cb = std::move(cb), end_stream]() mutable {
+ cb(nullptr, end_stream);
+ });
+ } else {
+ bs.callback_(nullptr, EndStream::Reset);
+ }
+ if (cache_sessions) {
+ cache_sessions->stats().subCacheSessionsSubscribers(1);
+ }
+ return true;
+ }
+ return false;
+ }),
+ body_subscribers_.end());
+}
+
+void CacheSession::maybeTriggerBodyReadForWaitingSubscriber() {
+ mu_.AssertHeld();
+ ASSERT(entry_.cache_reader_);
+ if (read_action_in_flight_) {
+ // There is already an action in flight so don't read more body yet.
+ return;
+ }
+ abortBodyOutOfRangeSubscribers();
+ auto it = std::find_if(
+ body_subscribers_.begin(), body_subscribers_.end(),
+ [this](BodySubscriber& subscriber) { return canReadBodyRangeFromCacheEntry(subscriber); });
+ if (it == body_subscribers_.end()) {
+ // There is nobody waiting to read some body that's available.
+ return;
+ }
+ AdjustedByteRange range = it->range_;
+ if (range.end() > body_length_available_) {
+ range = AdjustedByteRange(range.begin(), body_length_available_);
+ }
+ if (range.length() > max_read_chunk_size_) {
+ range = AdjustedByteRange(range.begin(), range.begin() + max_read_chunk_size_);
+ }
+ // Don't need this to be cancellable because there's a shared_ptr in the lambda keeping the
+ // CacheSession alive. We post to a thread before making the request for two reasons - we want
+ // the request to be performed on the requester's worker thread for balance, and we want to be
+ // able to lock the mutex again on the callback - if the cache called back immediately rather than
+ // posting and we *didn't* post before making the request, the mutex would still be held
+ // from this outer function so the callback would deadlock. By posting to a queue we ensure
+ // that deadlock cannot occur.
+ // Also, by ensuring the action occurs from a dispatcher queue, we guarantee that
+ // the "trigger again" at the end of onBodyChunkFromCache can't build up to a stack overflow
+ // of maybeTrigger->getBody->onBodyChunk->maybeTrigger->...
+ read_action_in_flight_ = true;
+ it->dispatcher().post([&dispatcher = it->dispatcher(), p = shared_from_this(), range,
+ cache_reader = entry_.cache_reader_.get()]() mutable {
+ cache_reader->getBody(
+ dispatcher, range,
+ [p = std::move(p), range](Buffer::InstancePtr buffer, EndStream end_stream) {
+ p->onBodyChunkFromCache(std::move(range), std::move(buffer), end_stream);
+ });
+ });
+}
+
+bool CacheSession::canReadBodyRangeFromCacheEntry(BodySubscriber& subscriber) {
+ mu_.AssertHeld();
+ return subscriber.range_.begin() < body_length_available_;
+}
+
+void CacheSession::onBodyChunkFromCache(AdjustedByteRange range, Buffer::InstancePtr buffer,
+ EndStream end_stream) {
+ absl::MutexLock lock(&mu_);
+ read_action_in_flight_ = false;
+ if (end_stream == EndStream::Reset) {
+ ENVOY_LOG(error, "cache entry provoked reset");
+ onCacheError();
+ return;
+ }
+ if (buffer == nullptr) {
+ IS_ENVOY_BUG("cache returned null buffer non-reset");
+ onCacheError();
+ return;
+ }
+ ASSERT(buffer->length() <= range.length());
+ if (buffer->length() < range.length()) {
+ range = AdjustedByteRange(range.begin(), range.begin() + buffer->length());
+ }
+ auto recipients_begin = std::partition(body_subscribers_.begin(), body_subscribers_.end(),
+ [&range](BodySubscriber& subscriber) {
+ return subscriber.range_.begin() < range.begin() ||
+ subscriber.range_.begin() >= range.end();
+ });
+ ASSERT(recipients_begin != body_subscribers_.end(),
+ "reading body chunk from cache with no corresponding request shouldn't happen");
+ if (std::next(recipients_begin) == body_subscribers_.end()) {
+ BodySubscriber& subscriber = *recipients_begin;
+ ASSERT(subscriber.range_.begin() == range.begin(),
+ "if there's only one matching subscriber it should have requested this precise chunk");
+ // There is only one recipient of this chunk, send it the actual buffer,
+ // no need to copy.
+ sendBodyChunkTo(subscriber,
+ AdjustedByteRange(subscriber.range_.begin(),
+ std::min(subscriber.range_.end(), range.end())),
+ std::move(buffer));
+ } else {
+ uint8_t* bytes = static_cast(buffer->linearize(range.length()));
+ for (auto it = recipients_begin; it != body_subscribers_.end(); it++) {
+ AdjustedByteRange r(it->range_.begin(), std::min(it->range_.end(), range.end()));
+ sendBodyChunkTo(
+ *it, r,
+ std::make_unique(bytes + r.begin() - range.begin(), r.length()));
+ }
+ }
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().subCacheSessionsSubscribers(
+ std::distance(recipients_begin, body_subscribers_.end()));
+ }
+ body_subscribers_.erase(recipients_begin, body_subscribers_.end());
+ maybeTriggerBodyReadForWaitingSubscriber();
+}
+
+void CacheSession::sendBodyChunkTo(BodySubscriber& subscriber, AdjustedByteRange range,
+ Buffer::InstancePtr buffer) {
+ mu_.AssertHeld();
+ bool end_stream = entry_.body_length_.has_value() && range.end() == entry_.body_length_.value() &&
+ entry_.response_trailers_ == nullptr;
+ subscriber.dispatcher().post([end_stream, callback = std::move(subscriber.callback_),
+ buffer = std::move(buffer)]() mutable {
+ callback(std::move(buffer), end_stream ? EndStream::End : EndStream::More);
+ });
+}
+
+CacheSession::~CacheSession() { ASSERT(!upstream_request_); }
+
+void CacheSession::getLookupResult(ActiveLookupRequestPtr lookup, ActiveLookupResultCallback&& cb) {
+ ASSERT(lookup->dispatcher().isThreadSafe());
+ absl::MutexLock lock(&mu_);
+ LookupSubscriber sub{std::make_unique(std::move(lookup), shared_from_this(),
+ content_length_header_),
+ std::move(cb)};
+ switch (state_) {
+ case State::Vary:
+ IS_ENVOY_BUG("not implemented yet");
+ ABSL_FALLTHROUGH_INTENDED;
+ case State::NotCacheable: {
+ postUpstreamPassThroughWithReset(std::move(sub), shared_from_this());
+ return;
+ }
+ case State::Validating:
+ case State::Pending:
+ sub.context_->lookup().stats().incCacheSessionsSubscribers();
+ lookup_subscribers_.push_back(std::move(sub));
+ return;
+ case State::Exists:
+ case State::Inserting: {
+ CacheEntryStatus status = CacheEntryStatus::Hit;
+ if (requiresValidationFor(sub.context_->lookup())) {
+ if (sub.context_->lookup().requestHeaders().getMethodValue() ==
+ Http::Headers::get().MethodValues.Head) {
+ // A HEAD request that requires validation can't write to the
+ // cache or use the cache entry, so just turn it into a pass-through.
+ return postUpstreamPassThrough(std::move(sub), CacheEntryStatus::Uncacheable);
+ }
+ if (state_ == State::Inserting) {
+ // Skip validation if the cache write is still in progress.
+ status = CacheEntryStatus::ValidatedFree;
+ } else {
+ sub.context_->lookup().stats().incCacheSessionsSubscribers();
+ lookup_subscribers_.push_back(std::move(sub));
+ return performValidation();
+ }
+ }
+ auto result = std::make_unique();
+ Event::Dispatcher& dispatcher = sub.dispatcher();
+ result->http_source_ = std::move(sub.context_);
+ result->status_ = status;
+ dispatcher.post([cb = std::move(sub.callback_), result = std::move(result)]() mutable {
+ cb(std::move(result));
+ });
+ return;
+ }
+ case State::New: {
+ Event::Dispatcher& dispatcher = sub.dispatcher();
+ if (sub.context_->lookup().requestHeaders().getMethodValue() ==
+ Http::Headers::get().MethodValues.Head) {
+ // HEAD requests are not cacheable, just pass through.
+ postUpstreamPassThrough(std::move(sub), CacheEntryStatus::Uncacheable);
+ return;
+ }
+ LookupRequest request(Key{sub.context_->lookup().key()}, dispatcher);
+ sub.context_->lookup().stats().incCacheSessionsSubscribers();
+ lookup_subscribers_.emplace_back(std::move(sub));
+ state_ = State::Pending;
+ std::shared_ptr cache_sessions = cache_sessions_.lock();
+ ASSERT(cache_sessions, "should be impossible for cache to be deleted in getLookupResult");
+ // posted to prevent callback mutex-deadlock.
+ return dispatcher.post([cache_sessions = std::move(cache_sessions), p = shared_from_this(),
+ request = std::move(request)]() mutable {
+ // p is captured as shared_ptr to ensure 'this' is not deleted while the
+ // lookup is in flight.
+ cache_sessions->cache().lookup(
+ std::move(request), [p = std::move(p)](absl::StatusOr&& lookup_result) {
+ p->onCacheLookupResult(std::move(lookup_result));
+ });
+ });
+ }
+ }
+}
+
+void CacheSession::onCacheLookupResult(absl::StatusOr&& lookup_result) {
+ absl::MutexLock lock(&mu_);
+ if (!lookup_result.ok()) {
+ return onCacheError();
+ }
+ entry_ = std::move(lookup_result.value());
+ if (!entry_.populated()) {
+ performUpstreamRequest();
+ } else {
+ state_ = State::Exists;
+ body_length_available_ = entry_.body_length_.value();
+ sendLookupResponsesAndMaybeValidationRequest();
+ }
+}
+
+void CacheSession::performUpstreamRequest() {
+ ENVOY_LOG(debug, "making upstream request to populate cache for {}", key_.path());
+ mu_.AssertHeld();
+ ASSERT(state_ == State::Pending);
+ ASSERT(
+ !lookup_subscribers_.empty(),
+ "upstream request should only be possible if someone requested a lookup and it was a miss");
+ ASSERT(!upstream_request_, "should only be one upstream request in flight");
+ LookupSubscriber& first_sub = lookup_subscribers_.front();
+ const ActiveLookupRequest& lookup = first_sub.context_->lookup();
+ Http::RequestHeaderMapPtr request_headers;
+ bool was_ranged_request = lookup.isRangeRequest();
+ if (was_ranged_request) {
+ request_headers = requestHeadersWithRangeRemoved(lookup.requestHeaders());
+ } else {
+ request_headers = Http::createHeaderMap(lookup.requestHeaders());
+ }
+ upstream_request_ = lookup.createUpstreamRequest();
+ first_sub.dispatcher().post([upstream_request = upstream_request_.get(),
+ request_headers = std::move(request_headers), this,
+ p = shared_from_this(), was_ranged_request]() mutable {
+ upstream_request->sendHeaders(std::move(request_headers));
+ upstream_request->getHeaders([this, p = std::move(p), was_ranged_request](
+ Http::ResponseHeaderMapPtr headers, EndStream end_stream) {
+ onUpstreamHeaders(std::move(headers), end_stream, was_ranged_request);
+ });
+ });
+}
+
+void CacheSession::onCacheWentAway() {
+ mu_.AssertHeld();
+ for (LookupSubscriber& sub : lookup_subscribers_) {
+ postUpstreamPassThrough(std::move(sub), CacheEntryStatus::LookupError);
+ }
+ lookup_subscribers_.clear();
+}
+
+void CacheSession::processSuccessfulValidation(Http::ResponseHeaderMapPtr headers) {
+ mu_.AssertHeld();
+ ENVOY_LOG(debug, "successful validation");
+ ASSERT(!lookup_subscribers_.empty(),
+ "should be impossible to be validating with no context awaiting validation");
+
+ const bool should_update_cached_entry =
+ CacheHeadersUtils::shouldUpdateCachedEntry(*headers, *entry_.response_headers_);
+ // Replace the 304 status code with the cached status code.
+ headers->setStatus(entry_.response_headers_->getStatusValue());
+
+ // Remove content length header if the 304 had one; if the cache entry had a
+ // content length header it will be added by the header adding block below.
+ headers->removeContentLength();
+
+ // A response that has been validated should not contain an Age header as it is equivalent to a
+ // freshly served response from the origin, unless the 304 response has an Age header, which
+ // means it was served by an upstream cache.
+ // Remove any existing Age header in the cached response.
+ entry_.response_headers_->removeInline(CacheCustomHeaders::age());
+
+ // Add any missing headers from the cached response to the 304 response.
+ entry_.response_headers_->iterate([&headers](const Http::HeaderEntry& cached_header) {
+ // TODO(yosrym93): see if we do this without copying the header key twice.
+ Http::LowerCaseString key(cached_header.key().getStringView());
+ if (headers->get(key).empty()) {
+ headers->setCopy(key, cached_header.value().getStringView());
+ }
+ return Http::HeaderMap::Iterate::Continue;
+ });
+
+ entry_.response_headers_ = std::move(headers);
+ state_ = State::Exists;
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ if (should_update_cached_entry) {
+ // TODO(yosrym93): else evict, set state to Pending, and treat as insert.
+ LookupSubscriber& sub = lookup_subscribers_.front();
+ // Update metadata associated with the cached response. Right now this is only
+ // response_time.
+ entry_.response_metadata_.response_time_ = cache_sessions->time_source_.systemTime();
+ cache_sessions->cache().updateHeaders(sub.dispatcher(), key_, *entry_.response_headers_,
+ entry_.response_metadata_);
+ }
+ }
+
+ CacheEntryStatus status = CacheEntryStatus::Validated;
+ for (LookupSubscriber& recipient : lookup_subscribers_) {
+ sendSuccessfulLookupResultTo(recipient, status);
+ // For requests sharing the same validation upstream, use a distinct status
+ // so it's detectable that we didn't need to do multiple validations.
+ status = CacheEntryStatus::ValidatedFree;
+ }
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().subCacheSessionsSubscribers(lookup_subscribers_.size());
+ }
+ lookup_subscribers_.clear();
+}
+
+void CacheSession::onUncacheable(Http::ResponseHeaderMapPtr headers, EndStream end_stream,
+ bool range_header_was_stripped) {
+ // If it turned out to be not cacheable, mark it as such, pass the already
+ // open connection to the first request, and give any other requests in flight
+ // a pass-through to upstream.
+ // If the upstream request stripped off a range header from the downstream
+ // request in order to populate the cache, we'll have to drop that upstream
+ // request and just issue a new request for every downstream.
+ mu_.AssertHeld();
+ state_ = State::NotCacheable;
+ bool use_existing_stream = !range_header_was_stripped;
+ if (!use_existing_stream) {
+ // Reset the upstream request if the request wanted a range and
+ // the upstream request didn't want a range.
+ upstream_request_ = nullptr;
+ }
+ for (LookupSubscriber& sub : lookup_subscribers_) {
+ sub.context_->setContentLength(content_length_header_);
+ if (use_existing_stream) {
+ ActiveLookupResultPtr result = std::make_unique();
+ result->status_ = CacheEntryStatus::Uncacheable;
+ result->http_source_ = std::make_unique(
+ std::move(upstream_request_), std::move(headers), end_stream);
+ sub.dispatcher().post([result = std::move(result), cb = std::move(sub.callback_)]() mutable {
+ cb(std::move(result));
+ });
+ use_existing_stream = false;
+ } else {
+ postUpstreamPassThrough(std::move(sub), CacheEntryStatus::Uncacheable);
+ }
+ }
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().subCacheSessionsSubscribers(lookup_subscribers_.size());
+ }
+ lookup_subscribers_.clear();
+ return;
+}
+
+void CacheSession::onUpstreamHeaders(Http::ResponseHeaderMapPtr headers, EndStream end_stream,
+ bool range_header_was_stripped) {
+ absl::MutexLock lock(&mu_);
+ Event::Dispatcher& dispatcher = lookup_subscribers_.front().dispatcher();
+ ASSERT(upstream_request_);
+ if (end_stream == EndStream::Reset) {
+ upstream_request_ = nullptr;
+ state_ = State::New;
+ for (LookupSubscriber& subscriber : lookup_subscribers_) {
+ subscriber.dispatcher().post([callback = std::move(subscriber.callback_)]() mutable {
+ auto result = std::make_unique();
+ result->status_ = CacheEntryStatus::UpstreamReset;
+ callback(std::move(result));
+ });
+ }
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->stats().subCacheSessionsSubscribers(lookup_subscribers_.size());
+ }
+ lookup_subscribers_.clear();
+ return;
+ }
+ ASSERT(headers);
+ if (state_ == State::Validating) {
+ if (Http::Utility::getResponseStatus(*headers) == enumToInt(Http::Code::NotModified)) {
+ upstream_request_ = nullptr;
+ return processSuccessfulValidation(std::move(headers));
+ } else {
+ // Validate failed, so going down the 'insert' path instead.
+ state_ = State::Pending;
+ if (auto cache_sessions = cache_sessions_.lock()) {
+ cache_sessions->cache().evict(dispatcher, key_);
+ }
+ body_length_available_ = 0;
+ entry_ = {};
+ }
+ } else {
+ ASSERT(state_ == State::Pending, "should only get upstreamHeaders for Validating or Pending");
+ }
+ absl::string_view cl = headers->getContentLengthValue();
+ if (!cl.empty()) {
+ absl::SimpleAtoi(cl, &content_length_header_) || (content_length_header_ = 0);
+ }
+ if (!lookup_subscribers_.front().context_->lookup().isCacheableResponse(*headers)) {
+ return onUncacheable(std::move(headers), end_stream, range_header_was_stripped);
+ }
+ if (VaryHeaderUtils::hasVary(*headers)) {
+ // TODO(ravenblack): implement Vary header support.
+ ENVOY_LOG(debug, "Vary header found in upstream response, treating as not cacheable");
+ return onUncacheable(std::move(headers), end_stream, range_header_was_stripped);
+ }
+ auto cache_sessions = cache_sessions_.lock();
+ if (!cache_sessions) {
+ // Cache was deleted while callback was in flight. As a fallback just make all
+ // requests pass through. This shouldn't happen, but it's possible that a config
+ // update can come in *and* the last filter using the cache can get
+ // downstream-disconnected and so deleted, leaving the upstream request
+ // dangling with no cache to talk to.
+ ENVOY_LOG(error, "cache config was deleted while upstream request was in flight");
+ return onCacheWentAway();
+ }
+ if (end_stream == EndStream::End) {
+ upstream_request_ = nullptr;
+ }
+ // We're already on this subscriber's thread; this is posted to ensure no
+ // deadlock on the mutex if the insert operation calls back directly.
+ lookup_subscribers_.front().dispatcher().post(
+ [p = shared_from_this(), &dispatcher = lookup_subscribers_.front().dispatcher(), key = key_,
+ cache_sessions, headers = std::move(headers),
+ upstream_request = std::move(upstream_request_)]() mutable {
+ cache_sessions->cache().insert(dispatcher, key, std::move(headers),
+ cache_sessions->makeMetadata(), std::move(upstream_request),
+ p);
+ // When the cache entry insertion completes it will call back to onHeadersInserted,
+ // or on error onInsertFailed.
+ });
+}
+
+void CacheSessionsImpl::lookup(ActiveLookupRequestPtr request, ActiveLookupResultCallback&& cb) {
+ ASSERT(request);
+ ASSERT(cb);
+ std::shared_ptr entry = getEntry(request->key());
+ entry->getLookupResult(std::move(request), std::move(cb));
+}
+
+ResponseMetadata CacheSessionsImpl::makeMetadata() {
+ ResponseMetadata metadata;
+ metadata.response_time_ = time_source_.systemTime();
+ return metadata;
+}
+
+void CacheSession::performValidation() {
+ mu_.AssertHeld();
+ ASSERT(!lookup_subscribers_.empty());
+ ENVOY_LOG(debug, "validating");
+ state_ = State::Validating;
+ LookupSubscriber& first_sub = lookup_subscribers_.front();
+ const ActiveLookupRequest& lookup = first_sub.context_->lookup();
+ Http::RequestHeaderMapPtr req = requestHeadersWithRangeRemoved(lookup.requestHeaders());
+ CacheHeadersUtils::injectValidationHeaders(*req, *entry_.response_headers_);
+ upstream_request_ = lookup.createUpstreamRequest();
+ first_sub.dispatcher().post([upstream_request = upstream_request_.get(), req = std::move(req),
+ this, p = shared_from_this()]() mutable {
+ upstream_request->sendHeaders(std::move(req));
+ upstream_request->getHeaders(
+ [this, p = std::move(p)](Http::ResponseHeaderMapPtr headers, EndStream end_stream) {
+ onUpstreamHeaders(std::move(headers), end_stream, false);
+ });
+ });
+}
+
+std::shared_ptr CacheSessionsImpl::getEntry(const Key& key) {
+ const SystemTime now = time_source_.systemTime();
+ cache().touch(key, now);
+ absl::MutexLock lock(&mu_);
+ auto [it, is_new] = entries_.try_emplace(key);
+ if (is_new) {
+ stats().incCacheSessionsEntries();
+ it->second = std::make_shared(weak_from_this(), key);
+ }
+ auto ret = it->second;
+ ret->setExpiry(now + expiry_duration_);
+ // As a lazy way of keeping the cache metadata from growing endlessly,
+ // remove at most one adjacent metadata entry every time an entry is touched
+ // if the adjacent entry hasn't been touched in a while.
+ // This should do a decent job of expiring them simply, with a low cost, and
+ // without taking any long-lived locks as would be required for periodic
+ // scanning.
+ if (++it == entries_.end()) {
+ it = entries_.begin();
+ }
+ if (it->second->isExpiredAt(now)) {
+ stats().decCacheSessionsEntries();
+ entries_.erase(it);
+ }
+ return ret;
+}
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cache_sessions_impl.h b/source/extensions/filters/http/cache/cache_sessions_impl.h
new file mode 100644
index 0000000000000..8883706eaabe9
--- /dev/null
+++ b/source/extensions/filters/http/cache/cache_sessions_impl.h
@@ -0,0 +1,310 @@
+#pragma once
+
+#include "envoy/buffer/buffer.h"
+
+#include "source/common/common/cancel_wrapper.h"
+#include "source/extensions/filters/http/cache/cache_sessions.h"
+#include "source/extensions/filters/http/cache/upstream_request.h"
+
+#include "absl/base/thread_annotations.h"
+#include "absl/container/flat_hash_map.h"
+#include "absl/synchronization/mutex.h"
+#include "stats.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+class CacheSession;
+class CacheSessionsImpl;
+
+class ActiveLookupContext : public HttpSource {
+public:
+ ActiveLookupContext(ActiveLookupRequestPtr lookup, std::shared_ptr entry,
+ uint64_t content_length = 0)
+ : lookup_(std::move(lookup)), entry_(entry), content_length_(content_length) {}
+ // HttpSource
+ void getHeaders(GetHeadersCallback&& cb) override;
+ void getBody(AdjustedByteRange range, GetBodyCallback&& cb) override;
+ void getTrailers(GetTrailersCallback&& cb) override;
+
+ Event::Dispatcher& dispatcher() const { return lookup().dispatcher(); }
+ ActiveLookupRequest& lookup() const { return *lookup_; }
+
+ void setContentLength(uint64_t l) { content_length_ = l; }
+
+private:
+ ActiveLookupRequestPtr lookup_;
+ std::shared_ptr entry_;
+ uint64_t content_length_;
+};
+
+class CacheSession : public Logger::Loggable,
+ public CacheProgressReceiver,
+ public std::enable_shared_from_this {
+public:
+ CacheSession(std::weak_ptr cache_sessions, const Key& key);
+
+ // CacheProgressReceiver
+ void onHeadersInserted(CacheReaderPtr cache_reader, Http::ResponseHeaderMapPtr headers,
+ bool end_stream) override;
+ void onBodyInserted(AdjustedByteRange range, bool end_stream) override;
+ void onTrailersInserted(Http::ResponseTrailerMapPtr trailers) override;
+ void onInsertFailed(absl::Status status) override;
+
+ void getLookupResult(ActiveLookupRequestPtr lookup,
+ ActiveLookupResultCallback&& lookup_result_callback)
+ ABSL_LOCKS_EXCLUDED(mu_);
+ void onCacheLookupResult(absl::StatusOr&& result) ABSL_LOCKS_EXCLUDED(mu_);
+
+ void wantHeaders(Event::Dispatcher& dispatcher, SystemTime lookup_timestamp,
+ GetHeadersCallback&& cb) ABSL_LOCKS_EXCLUDED(mu_);
+ void wantBodyRange(AdjustedByteRange range, Event::Dispatcher& dispatcher, GetBodyCallback&& cb)
+ ABSL_LOCKS_EXCLUDED(mu_);
+ void wantTrailers(Event::Dispatcher& dispatcher, GetTrailersCallback&& cb)
+ ABSL_LOCKS_EXCLUDED(mu_);
+ void clearUncacheableState() ABSL_LOCKS_EXCLUDED(mu_);
+
+ ~CacheSession();
+
+ class Subscriber {
+ public:
+ explicit Subscriber(Event::Dispatcher& dispatcher) : dispatcher_(dispatcher) {}
+ Event::Dispatcher& dispatcher() { return dispatcher_.get(); }
+
+ private:
+ // In order to be moveable in a vector we can't use a plain reference.
+ std::reference_wrapper dispatcher_;
+ };
+ class BodySubscriber : public Subscriber {
+ public:
+ BodySubscriber(Event::Dispatcher& dispatcher, AdjustedByteRange range, GetBodyCallback&& cb)
+ : Subscriber(dispatcher), callback_(std::move(cb)), range_(std::move(range)) {}
+ GetBodyCallback callback_;
+ AdjustedByteRange range_;
+ };
+ class TrailerSubscriber : public Subscriber {
+ public:
+ TrailerSubscriber(Event::Dispatcher& dispatcher, GetTrailersCallback&& cb)
+ : Subscriber(dispatcher), callback_(std::move(cb)) {}
+ GetTrailersCallback callback_;
+ };
+ class LookupSubscriber : public Subscriber {
+ public:
+ LookupSubscriber(std::unique_ptr context, ActiveLookupResultCallback&& cb)
+ : Subscriber(context->dispatcher()), callback_(std::move(cb)),
+ context_(std::move(context)) {}
+ ActiveLookupResultCallback callback_;
+ std::unique_ptr context_;
+ };
+
+private:
+ enum class State {
+ // New state means this is the first client of the cache entry - it should immediately
+ // update the state to Pending and attempt a lookup (then if necessary insertion).
+ New,
+ // Pending state means another client is already doing lookup/insertion/verification.
+ // Client should subscribe to this, and act on received messages.
+ Pending,
+ // Inserting state means a cache entry exists but has not yet completed writing.
+ Inserting,
+ // Exists state means a cache entry probably exists. Client should attempt to read from
+ // the entry. On cache failure, state should revert to New. On expiry, state should become
+ // Validating.
+ Exists,
+ // Validating state means the cache entry exists but either is expired or some header has
+ // explicitly required validation from upstream.
+ Validating,
+ // Vary state means the cache entry includes headers and the request must be
+ // re-keyed onto the appropriate variation key.
+ Vary,
+ // NotCacheable state means this key is considered non-cacheable. Client should pass through.
+ // If the passed-through response turns out to be cacheable (i.e. upstream has changed
+ // cache headers), client should update state to Writing, or, if state is already changed,
+ // client should abort the new upstream request and use the shared one.
+ NotCacheable
+ };
+
+ EndStream endStreamAfterHeaders() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ EndStream endStreamAfterBody() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ // Switches state to Written, removes the insert_context_, notifies all
+ // subscribers.
+ void insertComplete() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ // Switches state to New, removes the insert_context_, resets all subscribers.
+ // Ideally this shouldn't happen, but an unreliable upstream could cause it.
+ // TODO(ravenblackx): this could theoretically be improved with a retry process
+ // rather than resetting all the downstreams on error, but that's beyond MVP.
+ void insertAbort() ABSL_LOCKS_EXCLUDED(mu_);
+
+ void headersWritten(const Http::ResponseHeaderMap&& response_headers,
+ ResponseMetadata&& response_metadata,
+ absl::optional content_length_override, bool end_stream)
+ ABSL_LOCKS_EXCLUDED(mu_);
+
+ // Populates the headers in memory.
+ void saveHeaders(const Http::ResponseHeaderMap&& response_headers,
+ ResponseMetadata&& response_metadata, absl::optional content_length,
+ bool end_stream) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ bool requiresValidationFor(const ActiveLookupRequest& lookup) const
+ ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ // For each subscriber, either sends a lookup response (if validation passes), or
+ // triggers validation *once* for all subscribers for whom validation failed.
+ // If an insert occurred then first_status should be Miss, otherwise Hit.
+ void sendLookupResponsesAndMaybeValidationRequest(
+ CacheEntryStatus first_status = CacheEntryStatus::Hit) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ // Sends an upstream validation request.
+ void performValidation() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void processSuccessfulValidation(Http::ResponseHeaderMapPtr headers)
+ ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ // If the headers include vary, update all blocked subscribers with their new keys
+ // and returns true. Otherwise returns false.
+ bool handleVary(const Http::ResponseHeaderMap&& response_headers)
+ ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ // Called by the InsertContext.
+ // Updates the state to reflect the increased availability, and
+ // triggers a file-read action if there is a subscriber waiting on a body chunk
+ // within the available range, and no read file action is in flight.
+ void bodyWrittenTo(uint64_t sz, bool end_stream) ABSL_LOCKS_EXCLUDED(mu_);
+
+ // Called by the InsertContext.
+ // Populates the trailers in memory, and calls sendTrailers.
+ void trailersWritten(Http::ResponseTrailerMapPtr response_trailers) ABSL_LOCKS_EXCLUDED(mu_);
+
+ // Attempts to open the cache file.
+ //
+ // On failure notifies the first queued LookupContext of a cache miss, so
+ // the cache entry can be either populated or marked as uncacheable.
+ //
+ // On success, attempts to validate the cache entry.
+ //
+ // If it is valid, all queued LookupContexts are notified to use the file.
+ //
+ // If it is not valid, attempts to populate the cache entry.
+ //
+ // If attempt to populate the cache entry fails, marks as uncacheable,
+ // hands the UpstreamRequest to the first LookupContext, and notifies the
+ // rest of the queue that the result is uncacheable and they should bypass
+ // the cache, or, if the original request had a range header which was
+ // discarded for the UpstreamRequest, the UpstreamRequest is reset and *all*
+ // LookupContexts are notified to bypass the cache.
+ void sendSuccessfulLookupResultTo(LookupSubscriber& subscriber, CacheEntryStatus status)
+ ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void checkCacheEntryExistence(Event::Dispatcher& dispatcher) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void onCacheEntryExistence(LookupResult&& lookup_result) ABSL_LOCKS_EXCLUDED(mu_);
+ void sendBodyChunkTo(BodySubscriber& subscriber, AdjustedByteRange range, Buffer::InstancePtr buf)
+ ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void sendTrailersTo(TrailerSubscriber& subscriber) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void sendAbortTo(Subscriber& subscriber) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ bool tryEnqueueBodyChunk(BodySubscriber& subscriber) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ // If there's not already a read operation in flight and any requested
+ // range is within the available range, start an operation to
+ // read that range (prioritized by oldest subscriber).
+ void maybeTriggerBodyReadForWaitingSubscriber() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ bool selectBodyToRead() ABSL_LOCKS_EXCLUDED(mu_);
+ void abortBodyOutOfRangeSubscribers() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ bool canReadBodyRangeFromCacheEntry(BodySubscriber& subscriber);
+ void onBodyChunkFromCache(AdjustedByteRange range, Buffer::InstancePtr buffer,
+ EndStream end_stream) ABSL_LOCKS_EXCLUDED(mu_);
+ void onCacheError() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ void doCacheEntryInvalid() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void doCacheMiss() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void validateCacheEntry(Event::Dispatcher& dispatcher) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void performUpstreamRequest() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ void onUpstreamHeaders(Http::ResponseHeaderMapPtr headers, EndStream end_stream,
+ bool range_header_was_stripped) ABSL_LOCKS_EXCLUDED(mu_);
+ void onUncacheable(Http::ResponseHeaderMapPtr headers, EndStream end_stream,
+ bool range_header_was_stripped) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+ // For the unlikely case that cache config was modified while operations were in flight,
+ // requests still in the lookup state are transformed to pass-through.
+ // Requests for headers/body/trailers should be able to continue as the cache
+ // *entries* can outlive the cache object itself as long as they're in use.
+ void onCacheWentAway() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ // May change state from New to Pending, or from Written to Validating.
+ // When changing state, also makes the corresponding upstream request.
+ void mutateStateForHeaderRequest(const LookupRequest& lookup) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ bool headersAreReady() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
+
+ mutable absl::Mutex mu_;
+ State state_ ABSL_GUARDED_BY(mu_) = State::New;
+ uint64_t content_length_header_ = 0;
+ LookupResult entry_ ABSL_GUARDED_BY(mu_);
+ // While streaming this is a proxy for body_length_ which should not
+ // be populated in entry_ until the insert is complete.
+ uint64_t body_length_available_ = 0;
+ std::weak_ptr cache_sessions_;
+ Key key_;
+ bool in_body_loop_callback_ = false;
+
+ std::vector lookup_subscribers_ ABSL_GUARDED_BY(mu_);
+ std::vector body_subscribers_ ABSL_GUARDED_BY(mu_);
+ std::vector trailer_subscribers_ ABSL_GUARDED_BY(mu_);
+ UpstreamRequestPtr upstream_request_ ABSL_GUARDED_BY(mu_);
+ bool read_action_in_flight_ ABSL_GUARDED_BY(mu_) = false;
+
+ // The following fields and functions are only used by CacheSessions.
+ friend class CacheSessionsImpl;
+ bool inserting() const {
+ absl::MutexLock lock(&mu_);
+ return state_ == State::Inserting;
+ }
+ void setExpiry(SystemTime expiry) { expires_at_ = expiry; }
+ bool isExpiredAt(SystemTime t) const { return expires_at_ < t && !inserting(); }
+
+ SystemTime expires_at_; // This is guarded by CacheSessions's mutex.
+
+ // An arbitrary 256k limit on per-read fragment size.
+ // TODO(ravenblack): Make this configurable?
+ static constexpr uint64_t max_read_chunk_size_ = 256 * 1024;
+};
+
+class CacheSessionsImpl : public CacheSessions,
+ public std::enable_shared_from_this {
+public:
+ CacheSessionsImpl(Server::Configuration::FactoryContext& context,
+ std::unique_ptr cache)
+ : time_source_(context.serverFactoryContext().timeSource()), cache_(std::move(cache)),
+ stats_(generateStats(context.scope(), cache_->cacheInfo().name_)) {}
+
+ void lookup(ActiveLookupRequestPtr request, ActiveLookupResultCallback&& cb) override;
+ CacheFilterStats& stats() const override { return *stats_; }
+
+ ResponseMetadata makeMetadata();
+
+ HttpCache& cache() const override { return *cache_; }
+
+private:
+ // Returns an entry with the given key, creating it if necessary.
+ std::shared_ptr getEntry(const Key& key) ABSL_LOCKS_EXCLUDED(mu_);
+
+ TimeSource& time_source_;
+ std::unique_ptr cache_;
+ CacheFilterStatsPtr stats_;
+ std::chrono::duration expiry_duration_ = std::chrono::minutes(5);
+ mutable absl::Mutex mu_;
+ // If there turns out to be problematic contention on this mutex, this could
+ // easily be turned into a simple short-hash-keyed array of maps each with
+ // their own mutex. Since it's only held for a short time and is related to
+ // async operations, it seems unlikely that mutex contention would be a
+ // significant bottleneck.
+ absl::flat_hash_map, MessageUtil, MessageUtil>
+ entries_ ABSL_GUARDED_BY(mu_);
+
+ friend class CacheSession;
+};
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/cacheability_utils.cc b/source/extensions/filters/http/cache/cacheability_utils.cc
index 9f142f5b1859f..24eb8966ee778 100644
--- a/source/extensions/filters/http/cache/cacheability_utils.cc
+++ b/source/extensions/filters/http/cache/cacheability_utils.cc
@@ -31,7 +31,7 @@ const std::vector& conditionalHeaders() {
}
} // namespace
-bool CacheabilityUtils::canServeRequestFromCache(const Http::RequestHeaderMap& headers) {
+absl::Status CacheabilityUtils::canServeRequestFromCache(const Http::RequestHeaderMap& headers) {
const absl::string_view method = headers.getMethodValue();
const Http::HeaderValues& header_values = Http::Headers::get();
@@ -45,16 +45,31 @@ bool CacheabilityUtils::canServeRequestFromCache(const Http::RequestHeaderMap& h
// header fields can be ignored by caches and intermediaries.
for (auto conditional_header : conditionalHeaders()) {
if (!headers.get(*conditional_header).empty()) {
- return false;
+ return absl::InvalidArgumentError(*conditional_header);
}
}
// TODO(toddmgreer): Also serve HEAD requests from cache.
// Cache-related headers are checked in HttpCache::LookupRequest.
- return headers.Path() && headers.Host() &&
- !headers.getInline(CacheCustomHeaders::authorization()) &&
- (method == header_values.MethodValues.Get || method == header_values.MethodValues.Head) &&
- Http::Utility::schemeIsValid(headers.getSchemeValue());
+ if (!headers.Path()) {
+ return absl::InvalidArgumentError("no path");
+ }
+ if (!headers.Host()) {
+ return absl::InvalidArgumentError("no host");
+ }
+ if (headers.getInline(CacheCustomHeaders::authorization())) {
+ return absl::InvalidArgumentError("authorization");
+ }
+ if (method.empty()) {
+ return absl::InvalidArgumentError("no method");
+ }
+ if (method != header_values.MethodValues.Get && method != header_values.MethodValues.Head) {
+ return absl::InvalidArgumentError(method);
+ }
+ if (!Http::Utility::schemeIsValid(headers.getSchemeValue())) {
+ return absl::InvalidArgumentError("scheme");
+ }
+ return absl::OkStatus();
}
bool CacheabilityUtils::isCacheableResponse(const Http::ResponseHeaderMap& headers,
diff --git a/source/extensions/filters/http/cache/cacheability_utils.h b/source/extensions/filters/http/cache/cacheability_utils.h
index 8418011f08c2a..e551f4b72370a 100644
--- a/source/extensions/filters/http/cache/cacheability_utils.h
+++ b/source/extensions/filters/http/cache/cacheability_utils.h
@@ -4,6 +4,8 @@
#include "source/common/http/headers.h"
#include "source/extensions/filters/http/cache/cache_headers_utils.h"
+#include "absl/status/status.h"
+
namespace Envoy {
namespace Extensions {
namespace HttpFilters {
@@ -13,7 +15,7 @@ namespace CacheabilityUtils {
// This does not depend on cache-control headers as
// request cache-control headers only decide whether
// validation is required and whether the response can be cached.
-bool canServeRequestFromCache(const Http::RequestHeaderMap& headers);
+absl::Status canServeRequestFromCache(const Http::RequestHeaderMap& headers);
// Checks if a response can be stored in cache.
// Note that if a request is not cacheable according to 'canServeRequestFromCache'
diff --git a/source/extensions/filters/http/cache/config.cc b/source/extensions/filters/http/cache/config.cc
index 3e33af3543070..ef926828166d1 100644
--- a/source/extensions/filters/http/cache/config.cc
+++ b/source/extensions/filters/http/cache/config.cc
@@ -1,6 +1,8 @@
#include "source/extensions/filters/http/cache/config.h"
#include "source/extensions/filters/http/cache/cache_filter.h"
+#include "source/extensions/filters/http/cache/cache_sessions.h"
+#include "source/extensions/filters/http/cache/stats.h"
namespace Envoy {
namespace Extensions {
@@ -10,7 +12,7 @@ namespace Cache {
Http::FilterFactoryCb CacheFilterFactory::createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::http::cache::v3::CacheConfig& config,
const std::string& /*stats_prefix*/, Server::Configuration::FactoryContext& context) {
- std::shared_ptr cache;
+ std::shared_ptr cache;
if (!config.disabled().value()) {
if (!config.has_typed_config()) {
throw EnvoyException("at least one of typed_config or disabled must be set");
@@ -25,10 +27,10 @@ Http::FilterFactoryCb CacheFilterFactory::createFilterFactoryFromProtoTyped(
cache = http_cache_factory->getCache(config, context);
}
-
- return [config = std::make_shared(config, context.serverFactoryContext()),
- cache](Http::FilterChainFactoryCallbacks& callbacks) -> void {
- callbacks.addStreamFilter(std::make_shared(config, cache));
+ return [config = std::make_shared(config, std::move(cache),
+ context.serverFactoryContext())](
+ Http::FilterChainFactoryCallbacks& callbacks) -> void {
+ callbacks.addStreamFilter(std::make_shared(config));
};
}
diff --git a/source/extensions/filters/http/cache/filter_state.h b/source/extensions/filters/http/cache/filter_state.h
deleted file mode 100644
index a161aecde53db..0000000000000
--- a/source/extensions/filters/http/cache/filter_state.h
+++ /dev/null
@@ -1,36 +0,0 @@
-#pragma once
-
-namespace Envoy {
-namespace Extensions {
-namespace HttpFilters {
-namespace Cache {
-
-enum class FilterState {
- Initial,
-
- // Cache lookup found a cached response that requires validation.
- ValidatingCachedResponse,
-
- // Cache lookup found a fresh or validated cached response and it is being added to the encoding
- // stream.
- ServingFromCache,
-
- // The cached response was successfully added to the encoding stream (either during decoding or
- // encoding).
- ResponseServedFromCache,
-
- // The filter won't serve a response from the cache, whether because the request wasn't cacheable,
- // there was no response in cache, the response in cache couldn't be served, or the request was
- // terminated before the cached response could be written. This may be set during decoding or
- // encoding.
- NotServingFromCache,
-
- // CacheFilter::onDestroy has been called, the filter will be destroyed soon. Any triggered
- // callbacks should be ignored.
- Destroyed
-};
-
-} // namespace Cache
-} // namespace HttpFilters
-} // namespace Extensions
-} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/http_cache.cc b/source/extensions/filters/http/cache/http_cache.cc
index 1b1862bebd3b9..dffc940629138 100644
--- a/source/extensions/filters/http/cache/http_cache.cc
+++ b/source/extensions/filters/http/cache/http_cache.cc
@@ -23,139 +23,10 @@ namespace Extensions {
namespace HttpFilters {
namespace Cache {
-LookupRequest::LookupRequest(const Http::RequestHeaderMap& request_headers, SystemTime timestamp,
- const VaryAllowList& vary_allow_list,
- bool ignore_request_cache_control_header)
- : request_headers_(Http::createHeaderMap(request_headers)),
- vary_allow_list_(vary_allow_list), timestamp_(timestamp) {
- // 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::RequestHeaderMap "
- "with null Path.");
- ASSERT(request_headers.Host(), "Can't form cache lookup key for malformed Http::RequestHeaderMap "
- "with null Host.");
- absl::string_view scheme = request_headers.getSchemeValue();
- ASSERT(Http::Utility::schemeIsValid(request_headers.getSchemeValue()));
-
- if (!ignore_request_cache_control_header) {
- initializeRequestCacheControl(request_headers);
- }
- // TODO(toddmgreer): Let config determine whether to include scheme, host, and
- // query params.
-
- // TODO(toddmgreer): get cluster name.
- key_.set_cluster_name("cluster_name_goes_here");
- key_.set_host(std::string(request_headers.getHostValue()));
- key_.set_path(std::string(request_headers.getPathValue()));
- if (Http::Utility::schemeIsHttp(scheme)) {
- key_.set_scheme(Key::HTTP);
- } else if (Http::Utility::schemeIsHttps(scheme)) {
- key_.set_scheme(Key::HTTPS);
- }
-}
-
-// Unless this API is still alpha, calls to stableHashKey() must always return
-// the same result, or a way must be provided to deal with a complete cache
-// flush.
size_t stableHashKey(const Key& key) { return DeterministicProtoHash::hash(key); }
-void LookupRequest::initializeRequestCacheControl(const Http::RequestHeaderMap& request_headers) {
- const absl::string_view cache_control =
- request_headers.getInlineValue(CacheCustomHeaders::requestCacheControl());
- const absl::string_view pragma = request_headers.getInlineValue(CacheCustomHeaders::pragma());
-
- if (!cache_control.empty()) {
- request_cache_control_ = RequestCacheControl(cache_control);
- } else {
- // According to: https://httpwg.org/specs/rfc7234.html#header.pragma,
- // when Cache-Control header is missing, "Pragma:no-cache" is equivalent to
- // "Cache-Control:no-cache". Any other directives are ignored.
- request_cache_control_.must_validate_ = RequestCacheControl(pragma).must_validate_;
- }
-}
-
-bool LookupRequest::requiresValidation(const Http::ResponseHeaderMap& response_headers,
- SystemTime::duration response_age) const {
- // TODO(yosrym93): Store parsed response cache-control in cache instead of parsing it on every
- // lookup.
- const absl::string_view cache_control =
- response_headers.getInlineValue(CacheCustomHeaders::responseCacheControl());
- const ResponseCacheControl response_cache_control(cache_control);
-
- const bool request_max_age_exceeded = request_cache_control_.max_age_.has_value() &&
- request_cache_control_.max_age_.value() < response_age;
- if (response_cache_control.must_validate_ || request_cache_control_.must_validate_ ||
- request_max_age_exceeded) {
- // Either the request or response explicitly require validation, or a request max-age
- // requirement is not satisfied.
- return true;
- }
-
- // CacheabilityUtils::isCacheableResponse(..) guarantees that any cached response satisfies this.
- ASSERT(response_cache_control.max_age_.has_value() ||
- (response_headers.getInline(CacheCustomHeaders::expires()) && response_headers.Date()),
- "Cache entry does not have valid expiration data.");
-
- SystemTime::duration freshness_lifetime;
- if (response_cache_control.max_age_.has_value()) {
- freshness_lifetime = response_cache_control.max_age_.value();
- } else {
- const SystemTime expires_value =
- CacheHeadersUtils::httpTime(response_headers.getInline(CacheCustomHeaders::expires()));
- const SystemTime date_value = CacheHeadersUtils::httpTime(response_headers.Date());
- freshness_lifetime = expires_value - date_value;
- }
-
- if (response_age > freshness_lifetime) {
- // Response is stale, requires validation if
- // the response does not allow being served stale,
- // or the request max-stale directive does not allow it.
- const bool allowed_by_max_stale =
- request_cache_control_.max_stale_.has_value() &&
- request_cache_control_.max_stale_.value() > response_age - freshness_lifetime;
- return response_cache_control.no_stale_ || !allowed_by_max_stale;
- } else {
- // Response is fresh, requires validation only if there is an unsatisfied min-fresh requirement.
- const bool min_fresh_unsatisfied =
- request_cache_control_.min_fresh_.has_value() &&
- request_cache_control_.min_fresh_.value() > freshness_lifetime - response_age;
- return min_fresh_unsatisfied;
- }
-}
-
-LookupResult LookupRequest::makeLookupResult(Http::ResponseHeaderMapPtr&& response_headers,
- ResponseMetadata&& metadata,
- absl::optional content_length) const {
- // TODO(toddmgreer): Implement all HTTP caching semantics.
- ASSERT(response_headers);
- LookupResult result;
-
- // Assumption: Cache lookup time is negligible. Therefore, now == timestamp_
- const Seconds age =
- CacheHeadersUtils::calculateAge(*response_headers, metadata.response_time_, timestamp_);
- response_headers->setInline(CacheCustomHeaders::age(), std::to_string(age.count()));
-
- result.cache_entry_status_ = requiresValidation(*response_headers, age)
- ? CacheEntryStatus::RequiresValidation
- : CacheEntryStatus::Ok;
- result.headers_ = std::move(response_headers);
- if (content_length.has_value()) {
- result.content_length_ = content_length;
- } else {
- absl::string_view content_length_header = result.headers_->getContentLengthValue();
- int64_t length_from_header;
- if (!content_length_header.empty() &&
- absl::SimpleAtoi(content_length_header, &length_from_header)) {
- result.content_length_ = length_from_header;
- }
- }
- if (result.content_length_.has_value()) {
- result.range_details_ =
- RangeUtils::createRangeDetails(requestHeaders(), result.content_length_.value());
- }
-
- return result;
-}
+LookupRequest::LookupRequest(Key&& key, Event::Dispatcher& dispatcher)
+ : dispatcher_(dispatcher), key_(key) {}
} // namespace Cache
} // namespace HttpFilters
diff --git a/source/extensions/filters/http/cache/http_cache.h b/source/extensions/filters/http/cache/http_cache.h
index 8ae1d5d869d4e..0efa9a237c695 100644
--- a/source/extensions/filters/http/cache/http_cache.h
+++ b/source/extensions/filters/http/cache/http_cache.h
@@ -1,20 +1,18 @@
#pragma once
-#include
#include
#include
-#include "envoy/buffer/buffer.h"
#include "envoy/common/time.h"
#include "envoy/config/typed_config.h"
#include "envoy/extensions/filters/http/cache/v3/cache.pb.h"
#include "envoy/http/header_map.h"
#include "envoy/server/factory_context.h"
-#include "source/common/common/assert.h"
-#include "source/common/common/logger.h"
#include "source/extensions/filters/http/cache/cache_entry_utils.h"
#include "source/extensions/filters/http/cache/cache_headers_utils.h"
+#include "source/extensions/filters/http/cache/cache_progress_receiver.h"
+#include "source/extensions/filters/http/cache/http_source.h"
#include "source/extensions/filters/http/cache/key.pb.h"
#include "source/extensions/filters/http/cache/range_utils.h"
@@ -25,37 +23,18 @@ namespace Extensions {
namespace HttpFilters {
namespace Cache {
-// Result of a lookup operation, including cached headers and information needed
-// to serve a response based on it, or to attempt to validate.
-struct LookupResult {
- // If cache_entry_status_ == Unusable, none of the other members are
- // meaningful.
- CacheEntryStatus cache_entry_status_ = CacheEntryStatus::Unusable;
-
- // Headers of the cached response.
- Http::ResponseHeaderMapPtr headers_;
-
- // Size of the full response body. Cache filter will generate a content-length
- // header with this value, replacing any preexisting content-length header.
- // (This lets us dechunk responses as we insert them, then later serve them
- // with a content-length header.)
- // If the cache entry is still populating, and the cache supports streaming,
- // and the response had no content-length header, the content length may be
- // unknown at lookup-time.
- absl::optional content_length_;
+class CacheSessions;
+class CacheReader;
- // If the request is a range request, this struct indicates if the ranges can
- // be satisfied and which ranges are requested. nullopt indicates that this is
- // not a range request or the range header has been ignored.
- absl::optional range_details_;
-
- // Update the content length of the object and its response headers.
- void setContentLength(uint64_t new_length) {
- content_length_ = new_length;
- headers_->setContentLength(new_length);
- }
+// Result of a lookup operation.
+struct LookupResult {
+ std::unique_ptr cache_reader_;
+ std::unique_ptr response_headers_;
+ std::unique_ptr response_trailers_;
+ ResponseMetadata response_metadata_;
+ absl::optional body_length_;
+ bool populated() const { return body_length_.has_value(); }
};
-using LookupResultPtr = std::unique_ptr;
// Produces a hash of key that is consistent across restarts, architectures,
// builds, and configurations. Caches that store persistent entries based on a
@@ -76,234 +55,86 @@ size_t stableHashKey(const Key& key);
class LookupRequest {
public:
// Prereq: request_headers's Path(), Scheme(), and Host() are non-null.
- LookupRequest(const Http::RequestHeaderMap& request_headers, SystemTime timestamp,
- const VaryAllowList& vary_allow_list,
- bool ignore_request_cache_control_header = false);
-
- const RequestCacheControl& requestCacheControl() const { return request_cache_control_; }
+ LookupRequest(Key&& key, Event::Dispatcher& dispatcher);
// Caches may modify the key according to local needs, though care must be
// taken to ensure that meaningfully distinct responses have distinct keys.
const Key& key() const { return key_; }
- // WARNING: Incomplete--do not use in production (yet).
- // Returns a LookupResult suitable for sending to the cache filter's
- // LookupHeadersCallback. Specifically,
- // - LookupResult::cache_entry_status_ is set according to HTTP cache
- // validation logic.
- // - LookupResult::headers_ takes ownership of response_headers.
- // - LookupResult::content_length_ == content_length.
- // - LookupResult::response_ranges_ entries are satisfiable (as documented
- // there).
- LookupResult makeLookupResult(Http::ResponseHeaderMapPtr&& response_headers,
- ResponseMetadata&& metadata,
- absl::optional content_length) const;
-
- const Http::RequestHeaderMap& requestHeaders() const { return *request_headers_; }
- const VaryAllowList& varyAllowList() const { return vary_allow_list_; }
+ Event::Dispatcher& dispatcher() const { return dispatcher_; }
private:
- void initializeRequestCacheControl(const Http::RequestHeaderMap& request_headers);
- bool requiresValidation(const Http::ResponseHeaderMap& response_headers,
- SystemTime::duration age) const;
-
+ Event::Dispatcher& dispatcher_;
Key key_;
- std::vector request_range_spec_;
- Http::RequestHeaderMapPtr request_headers_;
- const VaryAllowList& vary_allow_list_;
- // Time when this LookupRequest was created (in response to an HTTP request).
- SystemTime timestamp_;
- RequestCacheControl request_cache_control_;
};
// Statically known information about a cache.
struct CacheInfo {
absl::string_view name_;
- bool supports_range_requests_ = false;
-};
-
-using LookupBodyCallback = absl::AnyInvocable;
-using LookupHeadersCallback = absl::AnyInvocable;
-using LookupTrailersCallback = absl::AnyInvocable;
-using InsertCallback = absl::AnyInvocable;
-using UpdateHeadersCallback = absl::AnyInvocable;
-
-// Manages the lifetime of an insertion.
-class InsertContext {
-public:
- // Accepts response_headers for caching. Only called once.
- //
- // Implementations MUST post to the filter's dispatcher insert_complete(true)
- // on success, or insert_complete(false) to attempt to abort the insertion.
- // This call may be made asynchronously, but any async operation that can
- // potentially silently fail must include a timeout, to avoid memory leaks.
- virtual void insertHeaders(const Http::ResponseHeaderMap& response_headers,
- const ResponseMetadata& metadata, InsertCallback insert_complete,
- bool end_stream) PURE;
-
- // The insertion is streamed into the cache in fragments whose size is determined
- // by the client, but with a pace determined by the cache. To avoid streaming
- // data into cache too fast for the cache to handle, clients should wait for
- // the cache to call ready_for_next_fragment before sending the next fragment.
- //
- // The client can abort the streaming insertion by dropping the
- // InsertContextPtr. A cache can abort the insertion by passing 'false' into
- // ready_for_next_fragment.
- //
- // The cache implementation MUST post ready_for_next_fragment to the filter's
- // dispatcher. This post may be made asynchronously, but any async operation
- // that can potentially silently fail must include a timeout, to avoid memory leaks.
- virtual void insertBody(const Buffer::Instance& fragment, InsertCallback ready_for_next_fragment,
- bool end_stream) PURE;
-
- // Inserts trailers into the cache.
- //
- // The cache implementation MUST post insert_complete to the filter's dispatcher.
- // This call may be made asynchronously, but any async operation that can
- // potentially silently fail must include a timeout, to avoid memory leaks.
- virtual void insertTrailers(const Http::ResponseTrailerMap& trailers,
- InsertCallback insert_complete) PURE;
-
- // This routine is called prior to an InsertContext being destroyed. InsertContext is responsible
- // for making sure that any async activities are cleaned up before returning from onDestroy().
- // This includes timers, network calls, etc. The reason there is an onDestroy() method vs. doing
- // this type of cleanup in the destructor is to avoid potential data races between an async
- // callback and the destructor in case the connection terminates abruptly.
- // Example scenario with a hypothetical cache that uses RPC:
- // 1. [Filter's thread] CacheFilter calls InsertContext::insertBody.
- // 2. [Filter's thread] RPCInsertContext sends RPC and returns.
- // 3. [Filter's thread] Client disconnects; Destroying stream; CacheFilter destructor begins.
- // 4. [Filter's thread] RPCInsertContext destructor begins.
- // 5. [Other thread] RPC completes and calls RPCInsertContext::onRPCDone.
- // --> RPCInsertContext's destructor and onRpcDone cause a data race in RpcInsertContext.
- // onDestroy() should cancel any outstanding async operations and, if necessary,
- // it should block on that cancellation to avoid data races. InsertContext must not invoke any
- // callbacks to the CacheFilter after returning from onDestroy().
- virtual void onDestroy() PURE;
-
- virtual ~InsertContext() = default;
};
-using InsertContextPtr = std::unique_ptr;
-// Lookup context manages the lifetime of a lookup, helping clients to pull data
-// from the cache at a pace that works for them. At any time a client can abort
-// an in-progress lookup by simply dropping the LookupContextPtr.
-class LookupContext {
+class CacheReader {
public:
- // Get the headers from the cache. It is a programming error to call this
- // twice.
- // In the case that a cache supports shared streaming (serving content from
- // the cache entry while it is still being populated), and a range request is made
- // for a streaming entry that didn't have a content-length header from upstream, range
- // requests may be unable to receive a response until the content-length is
- // known to exceed the end of the requested range. In this case a cache
- // implementation should wait until that is known before calling the callback,
- // and must pass a LookupResult with range_details_->satisfiable_ = false
- // if the request is invalid.
- //
- // A cache that posts the callback must wrap it such that if the LookupContext is
- // destroyed before the callback is executed, the callback is not executed.
- virtual void getHeaders(LookupHeadersCallback&& cb) PURE;
-
- // Reads the next fragment from the cache, calling cb when the fragment is ready.
- // The Buffer::InstancePtr passed to cb must not be null.
- //
- // The cache must call cb with a range of bytes starting at range.start() and
- // ending at or before range.end(). Caller is responsible for tracking what
- // ranges have been received, what to request next, and when to stop.
- //
- // A request may have a range that exceeds the size of the content, in support
- // of a "shared stream" cache entry, where the request may not know the size of
- // the content in advance. In this case the cache should call cb with
- // end_stream=true when the end of the body is reached, if there are no trailers.
- //
- // If there are trailers *and* the size of the content was not known when the
- // LookupContext was created, the cache should pass a null buffer pointer to the
- // LookupBodyCallback (when getBody is called with a range starting beyond the
- // end of the actual content-length) to indicate that no more body is available
- // and the filter should request trailers. It is invalid to pass a null buffer
- // pointer other than in this case.
- //
- // If a cache happens to load data in fragments of a set size, it may be
- // efficient to respond with fewer than the requested number of bytes. For
- // example, assuming a 23 byte full-bodied response from a cache that reads in
- // absurdly small 10 byte fragments:
- //
- // getBody requests bytes 0-23 .......... callback with bytes 0-9
- // getBody requests bytes 10-23 .......... callback with bytes 10-19
- // getBody requests bytes 20-23 .......... callback with bytes 20-23
- //
- // A cache that posts the callback must wrap it such that if the LookupContext is
- // destroyed before the callback is executed, the callback is not executed.
- virtual void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) PURE;
-
- // Get the trailers from the cache. Only called if the request reached the end of
- // the body and LookupBodyCallback did not pass true for end_stream. The
- // Http::ResponseTrailerMapPtr passed to cb must not be null.
- //
- // A cache that posts the callback must wrap it such that if the LookupContext is
- // destroyed before the callback is executed, the callback is not executed.
- virtual void getTrailers(LookupTrailersCallback&& cb) PURE;
-
- // This routine is called prior to a LookupContext being destroyed. LookupContext is responsible
- // for making sure that any async activities are cleaned up before returning from onDestroy().
- // This includes timers, network calls, etc. The reason there is an onDestroy() method vs. doing
- // this type of cleanup in the destructor is to avoid potential data races between an async
- // callback and the destructor in case the connection terminates abruptly.
- // Example scenario with a hypothetical cache that uses RPC:
- // 1. [Filter's thread] CacheFilter calls LookupContext::getHeaders.
- // 2. [Filter's thread] RPCLookupContext sends RPC and returns.
- // 3. [Filter's thread] Client disconnects; Destroying stream; CacheFilter destructor begins.
- // 4. [Filter's thread] RPCLookupContext destructor begins.
- // 5. [Other thread] RPC completes and calls RPCLookupContext::onRPCDone.
- // --> RPCLookupContext's destructor and onRpcDone cause a data race in RPCLookupContext.
- // onDestroy() should cancel any outstanding async operations and, if necessary,
- // it should block on that cancellation to avoid data races. LookupContext must not invoke any
- // callbacks to the CacheFilter after having onDestroy() invoked.
- virtual void onDestroy() PURE;
-
- virtual ~LookupContext() = default;
+ // May call the callback immediately; dispatcher is provided as an option to facilitate
+ // asynchronous operations.
+ // Will only be called with ranges the cache has announced are available, either via
+ // CacheProgressReceiver::onBodyInserted or via HttpCache::LookupCallback.
+ // end_stream should always be More, unless a cache error occurs in which case Reset -
+ // client already knows the body length so cache does not need to detect 'End'.
+ virtual void getBody(Event::Dispatcher& dispatcher, AdjustedByteRange range,
+ GetBodyCallback&& cb) PURE;
+ virtual ~CacheReader() = default;
};
-using LookupContextPtr = std::unique_ptr;
+using CacheReaderPtr = std::unique_ptr;
// Implement this interface to provide a cache implementation for use by
// CacheFilter.
class HttpCache {
public:
- // Returns a LookupContextPtr to manage the state of a cache lookup. On a cache
- // miss, the returned LookupContext will be given to the insert call (if any).
- //
- // It is possible for a cache to make a "shared stream" of responses allowing
- // read access to a cache entry before its write is complete. In this case the
- // content-length value may be unset.
- virtual LookupContextPtr makeLookupContext(LookupRequest&& request,
- Http::StreamFilterCallbacks& callbacks) PURE;
-
- // Returns an InsertContextPtr to manage the state of a cache insertion.
- // Responses with a chunked transfer-encoding must be dechunked before
- // insertion.
- virtual InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context,
- Http::StreamFilterCallbacks& callbacks) PURE;
-
- // Precondition: lookup_context represents a prior cache lookup that required
- // validation.
- //
- // Update the headers of that cache entry to match response_headers. The cache
- // entry's body and trailers (if any) will not be modified.
- //
- // This is called when an expired cache entry is successfully validated, to
- // update the cache entry.
- //
- // The on_complete callback is called with true if the update is successful,
- // false if the update was not performed.
- virtual void updateHeaders(const LookupContext& lookup_context,
- const Http::ResponseHeaderMap& response_headers,
- const ResponseMetadata& metadata,
- UpdateHeadersCallback on_complete) PURE;
+ // LookupCallback returns an empty LookupResult if the cache entry does not exist.
+ // Statuses are for actual errors.
+ using LookupCallback = absl::AnyInvocable&&)>;
// Returns statically known information about a cache.
virtual CacheInfo cacheInfo() const PURE;
+ // Calls the callback with a LookupResult; its body_length_ should be nullopt
+ // if the key was not found in the cache. Its cache_reader may be nullopt if the
+ // cache entry has no body.
+ // Using the dispatcher is optional, the callback is thread-safe.
+ // The callback must be called - if the cache is deleted while a callback
+ // is still in flight, the callback should be called with an error status.
+ virtual void lookup(LookupRequest&& request, LookupCallback&& callback) PURE;
+
+ // Remove the entry from the cache.
+ // This should accept any dispatcher, as the cache has no worker affinity.
+ virtual void evict(Event::Dispatcher& dispatcher, const Key& key) PURE;
+
+ // To facilitate LRU cache eviction, provide a timestamp whenever a cache entry is
+ // looked up.
+ virtual void touch(const Key& key, SystemTime timestamp) PURE;
+
+ // Replaces the headers in the cache.
+ // If this requires asynchronous operations, getBody must continue to function for the duration
+ // (perhaps reading from the existing data).
+ // This should avoid modifying the data in-place non-atomically, as during hot restart or other
+ // circumstances in which multiple instances are accessing the same cache, the data store could
+ // be read from while partially written.
+ // If the key doesn't exist, this should be a no-op.
+ virtual void updateHeaders(Event::Dispatcher& dispatcher, const Key& key,
+ const Http::ResponseHeaderMap& updated_headers,
+ const ResponseMetadata& updated_metadata) PURE;
+
+ // insert is only called after the headers have been read successfully and confirmed
+ // to be cacheable, so the headers are provided immediately as the HttpSource has
+ // already consumed them.
+ // If end_stream was true, HttpSourcePtr is null.
+ // The cache insert for future lookup() should only be completed atomically when the
+ // insertion is finished, while the CacheReader passed to progress->onHeadersInserted
+ // should be ready for streaming from immediately (subject to relevant body progress).
+ virtual void insert(Event::Dispatcher& dispatcher, Key key, Http::ResponseHeaderMapPtr headers,
+ ResponseMetadata metadata, HttpSourcePtr source,
+ std::shared_ptr progress) PURE;
virtual ~HttpCache() = default;
};
@@ -313,12 +144,12 @@ class HttpCacheFactory : public Config::TypedFactory {
// From UntypedFactory
std::string category() const override { return "envoy.http.cache"; }
- // Returns an HttpCache that will remain valid indefinitely (at least as long
- // as the calling CacheFilter).
+ // Returns a CacheSessions initialized with an HttpCache that will remain
+ // valid indefinitely (at least as long as the calling CacheFilter).
//
// Pass factory context to allow HttpCache to use async client, stats scope
// etc.
- virtual std::shared_ptr
+ virtual std::shared_ptr
getCache(const envoy::extensions::filters::http::cache::v3::CacheConfig& config,
Server::Configuration::FactoryContext& context) PURE;
diff --git a/source/extensions/filters/http/cache/http_source.h b/source/extensions/filters/http/cache/http_source.h
new file mode 100644
index 0000000000000..47ff6ab7b5197
--- /dev/null
+++ b/source/extensions/filters/http/cache/http_source.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include
+
+#include "envoy/buffer/buffer.h"
+#include "envoy/http/header_map.h"
+
+#include "source/extensions/filters/http/cache/range_utils.h"
+
+#include "absl/functional/any_invocable.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+// Reset indicates that the upstream source reset (or, if it's not a stream, some
+// kind of unexpected error).
+// More is equivalent to bool end_stream=false.
+// End is equivalent to bool end_stream=true.
+enum class EndStream { Reset, More, End };
+using GetHeadersCallback =
+ absl::AnyInvocable;
+using GetBodyCallback = absl::AnyInvocable;
+using GetTrailersCallback =
+ absl::AnyInvocable;
+
+// HttpSource is an interface for a source of HTTP data.
+// Callbacks can potentially be called before returning from the get* function.
+// The callback should be called on the same thread as the caller.
+// Only one request should be in flight at a time, and requests must be in
+// order as the source is assumed to be a stream (i.e. headers before body,
+// earlier body before later body, trailers last).
+class HttpSource {
+public:
+ // Calls the provided callback with http headers.
+ virtual void getHeaders(GetHeadersCallback&& cb) PURE;
+ // Calls the provided callback with a buffer that is the beginning of the
+ // requested range, up to but not necessarily including the entire requested
+ // range, or no buffer if there is no more data or an error occurred.
+ virtual void getBody(AdjustedByteRange range, GetBodyCallback&& cb) PURE;
+ virtual void getTrailers(GetTrailersCallback&& cb) PURE;
+ virtual ~HttpSource() = default;
+};
+
+using HttpSourcePtr = std::unique_ptr;
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/range_utils.cc b/source/extensions/filters/http/cache/range_utils.cc
index 6fab49640460e..7382aceed0a20 100644
--- a/source/extensions/filters/http/cache/range_utils.cc
+++ b/source/extensions/filters/http/cache/range_utils.cc
@@ -68,48 +68,41 @@ RangeUtils::getRangeHeader(const Envoy::Http::RequestHeaderMap& headers) {
RangeDetails
RangeUtils::createAdjustedRangeDetails(const std::vector& request_range_spec,
uint64_t content_length) {
- RangeDetails result;
if (request_range_spec.empty()) {
// No range header, so the request can proceed.
- result.satisfiable_ = true;
- return result;
+ return {true, {}};
}
if (content_length == 0) {
// There is a range header, but it's unsatisfiable.
- result.satisfiable_ = false;
- return result;
+ return {false, {}};
}
+ RangeDetails result;
for (const RawByteRange& spec : request_range_spec) {
if (spec.isSuffix()) {
// spec is a suffix-byte-range-spec.
if (spec.suffixLength() == 0) {
- // This range is unsatisfiable, so skip it.
- continue;
+ // This range is unsatisfiable.
+ return {false, {}};
}
if (spec.suffixLength() >= content_length) {
// All bytes are being requested, so we may as well send a '200
// OK' response.
- result.ranges_.clear();
- result.satisfiable_ = true;
- return result;
+ return {true, {}};
}
result.ranges_.emplace_back(content_length - spec.suffixLength(), content_length);
} else {
// spec is a byte-range-spec
if (spec.firstBytePos() >= content_length) {
- // This range is unsatisfiable, so skip it.
- continue;
+ // This range is unsatisfiable.
+ return {false, {}};
}
if (spec.lastBytePos() >= content_length - 1) {
if (spec.firstBytePos() == 0) {
// All bytes are being requested, so we may as well send a '200
// OK' response.
-
- result.ranges_.clear();
- result.satisfiable_ = true;
- return result;
+ return {true, {}};
}
result.ranges_.emplace_back(spec.firstBytePos(), content_length);
} else {
diff --git a/source/extensions/filters/http/cache/stats.cc b/source/extensions/filters/http/cache/stats.cc
new file mode 100644
index 0000000000000..7a63154f1d216
--- /dev/null
+++ b/source/extensions/filters/http/cache/stats.cc
@@ -0,0 +1,142 @@
+#include "source/extensions/filters/http/cache/stats.h"
+
+#include "envoy/stats/stats_macros.h"
+
+#include "absl/strings/str_replace.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+#define CACHE_FILTER_STATS(COUNTER, GAUGE, HISTOGRAM, TEXT_READOUT, STATNAME) \
+ STATNAME(cache_sessions_entries) \
+ STATNAME(cache_sessions_subscribers) \
+ STATNAME(upstream_buffered_bytes) \
+ STATNAME(cache) \
+ STATNAME(cache_label) \
+ STATNAME(event) \
+ STATNAME(event_type) \
+ STATNAME(hit) \
+ STATNAME(miss) \
+ STATNAME(failed_validation) \
+ STATNAME(uncacheable) \
+ STATNAME(upstream_reset) \
+ STATNAME(lookup_error) \
+ STATNAME(validate)
+
+MAKE_STAT_NAMES_STRUCT(CacheStatNames, CACHE_FILTER_STATS);
+
+using Envoy::Stats::Utility::counterFromStatNames;
+using Envoy::Stats::Utility::gaugeFromStatNames;
+
+class CacheFilterStatsImpl : public CacheFilterStats {
+public:
+ CacheFilterStatsImpl(Stats::Scope& scope, absl::string_view label)
+ : stat_names_(scope.symbolTable()), prefix_(stat_names_.cache_),
+ label_(stat_names_.pool_.add(absl::StrReplaceAll(label, {{".", "_"}}))),
+ tags_just_label_({{stat_names_.cache_label_, label_}}),
+ tags_hit_(
+ {{stat_names_.cache_label_, label_}, {stat_names_.event_type_, stat_names_.hit_}}),
+ tags_miss_(
+ {{stat_names_.cache_label_, label_}, {stat_names_.event_type_, stat_names_.miss_}}),
+ tags_failed_validation_({{stat_names_.cache_label_, label_},
+ {stat_names_.event_type_, stat_names_.failed_validation_}}),
+ tags_uncacheable_({{stat_names_.cache_label_, label_},
+ {stat_names_.event_type_, stat_names_.uncacheable_}}),
+ tags_upstream_reset_({{stat_names_.cache_label_, label_},
+ {stat_names_.event_type_, stat_names_.upstream_reset_}}),
+ tags_lookup_error_({{stat_names_.cache_label_, label_},
+ {stat_names_.event_type_, stat_names_.lookup_error_}}),
+ tags_validate_(
+ {{stat_names_.cache_label_, label_}, {stat_names_.event_type_, stat_names_.validate_}}),
+ gauge_cache_sessions_entries_(
+ gaugeFromStatNames(scope, {prefix_, stat_names_.cache_sessions_entries_},
+ Stats::Gauge::ImportMode::NeverImport, tags_just_label_)),
+ gauge_cache_sessions_subscribers_(
+ gaugeFromStatNames(scope, {prefix_, stat_names_.cache_sessions_subscribers_},
+ Stats::Gauge::ImportMode::NeverImport, tags_just_label_)),
+ gauge_upstream_buffered_bytes_(
+ gaugeFromStatNames(scope, {prefix_, stat_names_.upstream_buffered_bytes_},
+ Stats::Gauge::ImportMode::NeverImport, tags_just_label_)),
+ counter_hit_(counterFromStatNames(scope, {prefix_, stat_names_.event_}, tags_hit_)),
+ counter_miss_(counterFromStatNames(scope, {prefix_, stat_names_.event_}, tags_miss_)),
+ counter_failed_validation_(
+ counterFromStatNames(scope, {prefix_, stat_names_.event_}, tags_failed_validation_)),
+ counter_uncacheable_(
+ counterFromStatNames(scope, {prefix_, stat_names_.event_}, tags_uncacheable_)),
+ counter_upstream_reset_(
+ counterFromStatNames(scope, {prefix_, stat_names_.event_}, tags_upstream_reset_)),
+ counter_lookup_error_(
+ counterFromStatNames(scope, {prefix_, stat_names_.event_}, tags_lookup_error_)),
+ counter_validate_(
+ counterFromStatNames(scope, {prefix_, stat_names_.event_}, tags_validate_)) {}
+ void incForStatus(CacheEntryStatus status) override;
+ void incCacheSessionsEntries() override { gauge_cache_sessions_entries_.inc(); }
+ void decCacheSessionsEntries() override { gauge_cache_sessions_entries_.dec(); }
+ void incCacheSessionsSubscribers() override { gauge_cache_sessions_subscribers_.inc(); }
+ void subCacheSessionsSubscribers(uint64_t count) override {
+ gauge_cache_sessions_subscribers_.sub(count);
+ }
+ void addUpstreamBufferedBytes(uint64_t bytes) override {
+ gauge_upstream_buffered_bytes_.add(bytes);
+ }
+ void subUpstreamBufferedBytes(uint64_t bytes) override {
+ gauge_upstream_buffered_bytes_.sub(bytes);
+ }
+
+private:
+ CacheFilterStatsImpl(CacheFilterStatsImpl&) = delete;
+ CacheStatNames stat_names_;
+ const Stats::StatName prefix_;
+ const Stats::StatName label_;
+ const Stats::StatNameTagVector tags_just_label_;
+ const Stats::StatNameTagVector tags_hit_;
+ const Stats::StatNameTagVector tags_miss_;
+ const Stats::StatNameTagVector tags_failed_validation_;
+ const Stats::StatNameTagVector tags_uncacheable_;
+ const Stats::StatNameTagVector tags_upstream_reset_;
+ const Stats::StatNameTagVector tags_lookup_error_;
+ const Stats::StatNameTagVector tags_validate_;
+ Stats::Gauge& gauge_cache_sessions_entries_;
+ Stats::Gauge& gauge_cache_sessions_subscribers_;
+ Stats::Gauge& gauge_upstream_buffered_bytes_;
+ Stats::Counter& counter_hit_;
+ Stats::Counter& counter_miss_;
+ Stats::Counter& counter_failed_validation_;
+ Stats::Counter& counter_uncacheable_;
+ Stats::Counter& counter_upstream_reset_;
+ Stats::Counter& counter_lookup_error_;
+ Stats::Counter& counter_validate_;
+};
+
+CacheFilterStatsPtr generateStats(Stats::Scope& scope, absl::string_view label) {
+ return std::make_unique(scope, label);
+}
+
+void CacheFilterStatsImpl::incForStatus(CacheEntryStatus status) {
+ switch (status) {
+ case CacheEntryStatus::Miss:
+ return counter_miss_.inc();
+ case CacheEntryStatus::FailedValidation:
+ return counter_failed_validation_.inc();
+ case CacheEntryStatus::Hit:
+ case CacheEntryStatus::FoundNotModified:
+ case CacheEntryStatus::Follower:
+ case CacheEntryStatus::ValidatedFree:
+ return counter_hit_.inc();
+ case CacheEntryStatus::Validated:
+ return counter_validate_.inc();
+ case CacheEntryStatus::UpstreamReset:
+ return counter_upstream_reset_.inc();
+ case CacheEntryStatus::Uncacheable:
+ return counter_uncacheable_.inc();
+ case CacheEntryStatus::LookupError:
+ return counter_lookup_error_.inc();
+ }
+}
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/stats.h b/source/extensions/filters/http/cache/stats.h
new file mode 100644
index 0000000000000..82c36e1cbb9d3
--- /dev/null
+++ b/source/extensions/filters/http/cache/stats.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include
+
+#include "source/extensions/filters/http/cache/cache_entry_utils.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+class CacheFilterStats {
+public:
+ virtual void incForStatus(CacheEntryStatus status) PURE;
+ virtual void incCacheSessionsEntries() PURE;
+ virtual void decCacheSessionsEntries() PURE;
+ virtual void incCacheSessionsSubscribers() PURE;
+ virtual void subCacheSessionsSubscribers(uint64_t count) PURE;
+ virtual void addUpstreamBufferedBytes(uint64_t bytes) PURE;
+ virtual void subUpstreamBufferedBytes(uint64_t bytes) PURE;
+ virtual ~CacheFilterStats() = default;
+};
+
+class CacheFilterStatsProvider {
+public:
+ virtual CacheFilterStats& stats() const PURE;
+ virtual ~CacheFilterStatsProvider() = default;
+};
+
+using CacheFilterStatsPtr = std::unique_ptr;
+
+CacheFilterStatsPtr generateStats(Stats::Scope& scope, absl::string_view label);
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/upstream_request.cc b/source/extensions/filters/http/cache/upstream_request.cc
deleted file mode 100644
index 550b81c59a7f6..0000000000000
--- a/source/extensions/filters/http/cache/upstream_request.cc
+++ /dev/null
@@ -1,272 +0,0 @@
-#include "source/extensions/filters/http/cache/upstream_request.h"
-
-#include "source/common/common/enum_to_int.h"
-#include "source/common/http/utility.h"
-#include "source/extensions/filters/http/cache/cache_custom_headers.h"
-#include "source/extensions/filters/http/cache/cache_filter.h"
-#include "source/extensions/filters/http/cache/cacheability_utils.h"
-
-namespace Envoy {
-namespace Extensions {
-namespace HttpFilters {
-namespace Cache {
-
-namespace {
-inline bool isResponseNotModified(const Http::ResponseHeaderMap& response_headers) {
- return Http::Utility::getResponseStatus(response_headers) == enumToInt(Http::Code::NotModified);
-}
-} // namespace
-
-void UpstreamRequest::setFilterState(FilterState fs) {
- filter_state_ = fs;
- if (filter_ != nullptr && filter_->filter_state_ != FilterState::Destroyed) {
- filter_->filter_state_ = fs;
- }
-}
-
-void UpstreamRequest::setInsertStatus(InsertStatus is) {
- if (filter_ != nullptr && filter_->filter_state_ != FilterState::Destroyed) {
- filter_->insert_status_ = is;
- }
-}
-
-void UpstreamRequest::processSuccessfulValidation(Http::ResponseHeaderMapPtr response_headers) {
- ASSERT(lookup_result_, "CacheFilter trying to validate a non-existent lookup result");
- ASSERT(
- filter_state_ == FilterState::ValidatingCachedResponse,
- "processSuccessfulValidation must only be called when a cached response is being validated");
- ASSERT(isResponseNotModified(*response_headers),
- "processSuccessfulValidation must only be called with 304 responses");
-
- // Check whether the cached entry should be updated before modifying the 304 response.
- const bool should_update_cached_entry = shouldUpdateCachedEntry(*response_headers);
-
- setFilterState(FilterState::ServingFromCache);
-
- // Replace the 304 response status code with the cached status code.
- response_headers->setStatus(lookup_result_->headers_->getStatusValue());
-
- // Remove content length header if the 304 had one; if the cache entry had a
- // content length header it will be added by the header adding block below.
- response_headers->removeContentLength();
-
- // A response that has been validated should not contain an Age header as it is equivalent to a
- // freshly served response from the origin, unless the 304 response has an Age header, which
- // means it was served by an upstream cache.
- // Remove any existing Age header in the cached response.
- lookup_result_->headers_->removeInline(CacheCustomHeaders::age());
-
- // Add any missing headers from the cached response to the 304 response.
- lookup_result_->headers_->iterate([&response_headers](const Http::HeaderEntry& cached_header) {
- // TODO(yosrym93): Try to avoid copying the header key twice.
- Http::LowerCaseString key(cached_header.key().getStringView());
- absl::string_view value = cached_header.value().getStringView();
- if (response_headers->get(key).empty()) {
- response_headers->setCopy(key, value);
- }
- return Http::HeaderMap::Iterate::Continue;
- });
-
- if (should_update_cached_entry) {
- // TODO(yosrym93): else the cached entry should be deleted.
- // Update metadata associated with the cached response. Right now this is only response_time;
- const ResponseMetadata metadata = {config_->timeSource().systemTime()};
- cache_->updateHeaders(*lookup_, *response_headers, metadata,
- [](bool updated ABSL_ATTRIBUTE_UNUSED) {});
- setInsertStatus(InsertStatus::HeaderUpdate);
- }
-
- // A cache entry was successfully validated, so abort the upstream request, send
- // encode the merged-modified headers, and encode cached body and trailers.
- if (filter_ != nullptr) {
- lookup_result_->headers_ = std::move(response_headers);
- filter_->lookup_result_ = std::move(lookup_result_);
- filter_->lookup_ = std::move(lookup_);
- filter_->upstream_request_ = nullptr;
- lookup_result_ = nullptr;
- filter_->encodeCachedResponse(/* end_stream_after_headers = */ false);
- filter_ = nullptr;
- abort();
- }
-}
-
-// TODO(yosrym93): Write a test that exercises this when SimpleHttpCache implements updateHeaders
-bool UpstreamRequest::shouldUpdateCachedEntry(
- const Http::ResponseHeaderMap& response_headers) const {
- ASSERT(isResponseNotModified(response_headers),
- "shouldUpdateCachedEntry must only be called with 304 responses");
- ASSERT(lookup_result_, "shouldUpdateCachedEntry precondition unsatisfied: lookup_result_ "
- "does not point to a cache lookup result");
- ASSERT(filter_state_ == FilterState::ValidatingCachedResponse,
- "shouldUpdateCachedEntry precondition unsatisfied: the "
- "CacheFilter is not validating a cache lookup result");
-
- // According to: https://httpwg.org/specs/rfc7234.html#freshening.responses,
- // and assuming a single cached response per key:
- // If the 304 response contains a strong validator (etag) that does not match the cached response,
- // the cached response should not be updated.
- const Http::HeaderEntry* response_etag = response_headers.getInline(CacheCustomHeaders::etag());
- const Http::HeaderEntry* cached_etag =
- lookup_result_->headers_->getInline(CacheCustomHeaders::etag());
- return !response_etag || (cached_etag && cached_etag->value().getStringView() ==
- response_etag->value().getStringView());
-}
-
-UpstreamRequest* UpstreamRequest::create(CacheFilter* filter, LookupContextPtr lookup,
- LookupResultPtr lookup_result,
- std::shared_ptr cache,
- Http::AsyncClient& async_client,
- const Http::AsyncClient::StreamOptions& options) {
- return new UpstreamRequest(filter, std::move(lookup), std::move(lookup_result), std::move(cache),
- async_client, options);
-}
-
-UpstreamRequest::UpstreamRequest(CacheFilter* filter, LookupContextPtr lookup,
- LookupResultPtr lookup_result, std::shared_ptr cache,
- Http::AsyncClient& async_client,
- const Http::AsyncClient::StreamOptions& options)
- : filter_(filter), lookup_(std::move(lookup)), lookup_result_(std::move(lookup_result)),
- is_head_request_(filter->is_head_request_),
- request_allows_inserts_(filter->request_allows_inserts_), config_(filter->config_),
- filter_state_(filter->filter_state_), cache_(std::move(cache)),
- stream_(async_client.start(*this, options)) {
- ASSERT(stream_ != nullptr);
-}
-
-void UpstreamRequest::insertQueueOverHighWatermark() {
- // TODO(ravenblack): currently AsyncRequest::Stream does not support pausing.
-}
-
-void UpstreamRequest::insertQueueUnderLowWatermark() {
- // TODO(ravenblack): currently AsyncRequest::Stream does not support pausing.
-}
-
-void UpstreamRequest::insertQueueAborted() {
- insert_queue_ = nullptr;
- ENVOY_LOG(debug, "cache aborted insert operation");
- setInsertStatus(InsertStatus::InsertAbortedByCache);
- if (filter_ == nullptr) {
- abort();
- }
-}
-
-void UpstreamRequest::sendHeaders(Http::RequestHeaderMap& request_headers) {
- // If this request had a body or trailers, CacheFilter::decodeHeaders
- // would have bypassed cache lookup and insertion, so this class wouldn't
- // be instantiated. So end_stream will always be true.
- stream_->sendHeaders(request_headers, true);
-}
-
-void UpstreamRequest::abort() {
- stream_->reset(); // Calls onReset, resulting in deletion.
-}
-
-UpstreamRequest::~UpstreamRequest() {
- if (filter_ != nullptr) {
- filter_->onUpstreamRequestReset();
- }
- if (lookup_) {
- lookup_->onDestroy();
- lookup_ = nullptr;
- }
- if (insert_queue_) {
- // The insert queue may still have actions in flight, so it needs to be allowed
- // to drain itself before destruction.
- insert_queue_->setSelfOwned(std::move(insert_queue_));
- }
-}
-
-void UpstreamRequest::onReset() { delete this; }
-void UpstreamRequest::onComplete() {
- if (filter_) {
- ENVOY_STREAM_LOG(debug, "UpstreamRequest complete", *filter_->decoder_callbacks_);
- filter_->onUpstreamRequestComplete();
- filter_ = nullptr;
- } else {
- ENVOY_LOG(debug, "UpstreamRequest complete after stream finished");
- }
- delete this;
-}
-void UpstreamRequest::disconnectFilter() {
- filter_ = nullptr;
- if (insert_queue_ == nullptr) {
- abort();
- }
-}
-
-void UpstreamRequest::onHeaders(Http::ResponseHeaderMapPtr&& headers, bool end_stream) {
- if (filter_state_ == FilterState::ValidatingCachedResponse && isResponseNotModified(*headers)) {
- return processSuccessfulValidation(std::move(headers));
- }
- // Either a cache miss or a cache entry that is no longer valid.
- // Check if the new response can be cached.
- if (request_allows_inserts_ && !is_head_request_ &&
- CacheabilityUtils::isCacheableResponse(*headers, config_->varyAllowList())) {
- if (filter_) {
- ENVOY_STREAM_LOG(debug, "UpstreamRequest::onHeaders inserting headers",
- *filter_->decoder_callbacks_);
- }
- auto insert_context =
- cache_->makeInsertContext(std::move(lookup_), *filter_->encoder_callbacks_);
- lookup_ = nullptr;
- if (insert_context != nullptr) {
- // The callbacks passed to CacheInsertQueue are all called through the dispatcher,
- // so they're thread-safe. During CacheFilter::onDestroy the queue is given ownership
- // of itself and all the callbacks are cancelled, so they are also filter-destruction-safe.
- insert_queue_ = std::make_unique(cache_, *filter_->encoder_callbacks_,
- std::move(insert_context), *this);
- // Add metadata associated with the cached response. Right now this is only response_time;
- const ResponseMetadata metadata = {config_->timeSource().systemTime()};
- insert_queue_->insertHeaders(*headers, metadata, end_stream);
- // insert_status_ remains absl::nullopt if end_stream == false, as we have not completed the
- // insertion yet.
- if (end_stream) {
- setInsertStatus(InsertStatus::InsertSucceeded);
- }
- }
- } else {
- setInsertStatus(InsertStatus::NoInsertResponseNotCacheable);
- }
- setFilterState(FilterState::NotServingFromCache);
- if (filter_) {
- filter_->decoder_callbacks_->encodeHeaders(std::move(headers), is_head_request_ || end_stream,
- StreamInfo::ResponseCodeDetails::get().ViaUpstream);
- }
-}
-
-void UpstreamRequest::onData(Buffer::Instance& body, bool end_stream) {
- if (insert_queue_ != nullptr) {
- insert_queue_->insertBody(body, end_stream);
- }
- if (filter_) {
- ENVOY_STREAM_LOG(debug, "UpstreamRequest::onData inserted body", *filter_->decoder_callbacks_);
- filter_->decoder_callbacks_->encodeData(body, end_stream);
- if (end_stream) {
- // We don't actually know at this point if the insert succeeded, but as far as the
- // filter is concerned it has been fully handed off to the cache
- // implementation.
- setInsertStatus(InsertStatus::InsertSucceeded);
- }
- } else {
- ENVOY_LOG(debug, "UpstreamRequest::onData inserted body");
- }
-}
-
-void UpstreamRequest::onTrailers(Http::ResponseTrailerMapPtr&& trailers) {
- if (insert_queue_ != nullptr) {
- insert_queue_->insertTrailers(*trailers);
- }
- if (filter_ != nullptr) {
- ENVOY_STREAM_LOG(debug, "UpstreamRequest::onTrailers inserting trailers",
- *filter_->decoder_callbacks_);
- filter_->decoder_callbacks_->encodeTrailers(std::move(trailers));
- setInsertStatus(InsertStatus::InsertSucceeded);
- } else {
- ENVOY_LOG(debug, "UpstreamRequest::onTrailers inserting trailers");
- }
-}
-
-} // namespace Cache
-} // namespace HttpFilters
-} // namespace Extensions
-} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/upstream_request.h b/source/extensions/filters/http/cache/upstream_request.h
index 6aa6259ca26cd..4b5a49d6748ac 100644
--- a/source/extensions/filters/http/cache/upstream_request.h
+++ b/source/extensions/filters/http/cache/upstream_request.h
@@ -1,83 +1,40 @@
#pragma once
-#include "source/common/common/logger.h"
-#include "source/extensions/filters/http/cache/cache_filter_logging_info.h"
-#include "source/extensions/filters/http/cache/cache_insert_queue.h"
+#include "source/extensions/filters/http/cache/http_source.h"
namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace Cache {
-class CacheFilter;
-class CacheFilterConfig;
-enum class FilterState;
+class CacheFilterStatsProvider;
-class UpstreamRequest : public Logger::Loggable,
- public Http::AsyncClient::StreamCallbacks,
- public InsertQueueCallbacks {
+class UpstreamRequest : public HttpSource {
public:
- void sendHeaders(Http::RequestHeaderMap& request_headers);
- // Called by filter_ when filter_ is destroyed first.
- // UpstreamRequest will make no more calls to filter_ once disconnectFilter
- // has been called.
- void disconnectFilter();
-
- // StreamCallbacks
- void onHeaders(Http::ResponseHeaderMapPtr&& headers, bool end_stream) override;
- void onData(Buffer::Instance& data, bool end_stream) override;
- void onTrailers(Http::ResponseTrailerMapPtr&& trailers) override;
- void onComplete() override;
- void onReset() override;
-
- // InsertQueueCallbacks
- void insertQueueOverHighWatermark() override;
- void insertQueueUnderLowWatermark() override;
- void insertQueueAborted() override;
-
- static UpstreamRequest* create(CacheFilter* filter, LookupContextPtr lookup,
- LookupResultPtr lookup_result, std::shared_ptr cache,
- Http::AsyncClient& async_client,
- const Http::AsyncClient::StreamOptions& options);
- UpstreamRequest(CacheFilter* filter, LookupContextPtr lookup, LookupResultPtr lookup_result,
- std::shared_ptr cache, Http::AsyncClient& async_client,
- const Http::AsyncClient::StreamOptions& options);
- ~UpstreamRequest() override;
-
-private:
- // Precondition: lookup_result_ points to a cache lookup result that requires validation.
- // filter_state_ is ValidatingCachedResponse.
- // Serves a validated cached response after updating it with a 304 response.
- void processSuccessfulValidation(Http::ResponseHeaderMapPtr response_headers);
-
- // Updates the filter state belonging to the UpstreamRequest, and the one belonging to
- // the filter if it has not been destroyed.
- void setFilterState(FilterState fs);
-
- // Updates the insert status belonging to the filter, if it has not been destroyed.
- void setInsertStatus(InsertStatus is);
-
- // If an error occurs while the stream is active, abort will reset the stream, which
- // in turn provokes the rest of the destruction process.
- void abort();
-
- // Precondition: lookup_result_ points to a cache lookup result that requires validation.
- // filter_state_ is ValidatingCachedResponse.
- // Checks if a cached entry should be updated with a 304 response.
- bool shouldUpdateCachedEntry(const Http::ResponseHeaderMap& response_headers) const;
+ virtual void sendHeaders(Http::RequestHeaderMapPtr headers) PURE;
+};
- CacheFilter* filter_ = nullptr;
- LookupContextPtr lookup_;
- LookupResultPtr lookup_result_;
- bool is_head_request_;
- bool request_allows_inserts_;
- std::shared_ptr config_;
- FilterState filter_state_;
- std::shared_ptr cache_;
- Http::AsyncClient::Stream* stream_ = nullptr;
- std::unique_ptr insert_queue_;
+using UpstreamRequestPtr = std::unique_ptr;
+
+// UpstreamRequest acts as a bridge between the "pull" operations preferred by
+// the cache filter (getHeaders/getBody/getTrailers) and the "push" operations
+// preferred by most of envoy (encodeHeaders etc. being called by the source).
+//
+// In order to bridge the two, UpstreamRequest must act as a buffer; on a get*
+// request it calls back only when the buffer has [some of] the requested data
+// in it; if the buffer gets overfull, watermark events are triggered on the
+// upstream. The client side should only send get* requests when it is ready for
+// more data, so the downstream is automatically resilient to OOM.
+// TODO(#33319): AsyncClient::Stream does not currently support watermark events.
+class UpstreamRequestFactory {
+public:
+ virtual UpstreamRequestPtr
+ create(const std::shared_ptr stats_provider) PURE;
+ virtual ~UpstreamRequestFactory() = default;
};
+using UpstreamRequestFactoryPtr = std::unique_ptr;
+
} // namespace Cache
} // namespace HttpFilters
} // namespace Extensions
diff --git a/source/extensions/filters/http/cache/upstream_request_impl.cc b/source/extensions/filters/http/cache/upstream_request_impl.cc
new file mode 100644
index 0000000000000..a91c2456747b5
--- /dev/null
+++ b/source/extensions/filters/http/cache/upstream_request_impl.cc
@@ -0,0 +1,212 @@
+#include "source/extensions/filters/http/cache/upstream_request_impl.h"
+
+#include "range_utils.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+UpstreamRequestPtr UpstreamRequestImplFactory::create(
+ const std::shared_ptr stats_provider) {
+ // Can't use make_unique because the constructor is private.
+ auto ret = std::unique_ptr(new UpstreamRequestImpl(
+ dispatcher_, async_client_, stream_options_, std::move(stats_provider)));
+ return ret;
+}
+
+UpstreamRequestImpl::UpstreamRequestImpl(
+ Event::Dispatcher& dispatcher, Http::AsyncClient& async_client,
+ const Http::AsyncClient::StreamOptions& options,
+ const std::shared_ptr stats_provider)
+ : dispatcher_(dispatcher), stream_(async_client.start(*this, options)),
+ body_buffer_([this]() { onBelowLowWatermark(); }, [this]() { onAboveHighWatermark(); },
+ nullptr),
+ stats_provider_(std::move(stats_provider)) {
+ ASSERT(stream_ != nullptr);
+ body_buffer_.setWatermarks(options.buffer_limit_.value_or(0));
+}
+
+void UpstreamRequestImpl::onAboveHighWatermark() {
+ ASSERT(dispatcher_.isThreadSafe());
+ // TODO(ravenblack): currently AsyncRequest::Stream does not support pausing.
+ // Waiting on issue #33319
+}
+
+void UpstreamRequestImpl::onBelowLowWatermark() {
+ ASSERT(dispatcher_.isThreadSafe());
+ // TODO(ravenblack): currently AsyncRequest::Stream does not support pausing.
+ // Waiting on issue #33319
+}
+
+void UpstreamRequestImpl::getHeaders(GetHeadersCallback&& cb) {
+ ASSERT(dispatcher_.isThreadSafe());
+ ASSERT(absl::holds_alternative(callback_));
+ if (!stream_ && !end_stream_after_headers_ && !end_stream_after_body_ && !trailers_) {
+ return cb(nullptr, EndStream::Reset);
+ }
+ callback_ = std::move(cb);
+ return maybeDeliverHeaders();
+}
+
+void UpstreamRequestImpl::onHeaders(Http::ResponseHeaderMapPtr&& headers, bool end_stream) {
+ ASSERT(dispatcher_.isThreadSafe());
+ headers_ = std::move(headers);
+ end_stream_after_headers_ = end_stream;
+ return maybeDeliverHeaders();
+}
+
+void UpstreamRequestImpl::maybeDeliverHeaders() {
+ ASSERT(dispatcher_.isThreadSafe());
+ if (!absl::holds_alternative(callback_) || !headers_) {
+ return;
+ }
+ return absl::get(consumeCallback())(
+ std::move(headers_), end_stream_after_headers_ ? EndStream::End : EndStream::More);
+}
+
+void UpstreamRequestImpl::getBody(AdjustedByteRange range, GetBodyCallback&& cb) {
+ ASSERT(dispatcher_.isThreadSafe());
+ ASSERT(absl::holds_alternative(callback_));
+ ASSERT(range.begin() == stream_pos_, "UpstreamRequest does not support out of order reads");
+ ASSERT(!end_stream_after_headers_);
+ if (!stream_ && !end_stream_after_body_ && !trailers_) {
+ return cb(nullptr, EndStream::Reset);
+ }
+ requested_body_range_ = std::move(range);
+ callback_ = std::move(cb);
+ return maybeDeliverBody();
+}
+
+void UpstreamRequestImpl::onData(Buffer::Instance& data, bool end_stream) {
+ ASSERT(dispatcher_.isThreadSafe());
+ end_stream_after_body_ = end_stream;
+ stats().addUpstreamBufferedBytes(data.length());
+ body_buffer_.move(data);
+ return maybeDeliverBody();
+}
+
+void UpstreamRequestImpl::maybeDeliverBody() {
+ ASSERT(dispatcher_.isThreadSafe());
+ if (!absl::holds_alternative(callback_)) {
+ return;
+ }
+ uint64_t len = std::min(requested_body_range_.length(), body_buffer_.length());
+ if (len == 0) {
+ if (trailers_) {
+ // If we've already seen trailers from upstream and there's no more buffered
+ // body, but the client is still requesting body, it means the client didn't
+ // know how much body to expect. A null body with end_stream=false informs the
+ // client to move on to requesting trailers.
+ return absl::get(consumeCallback())(nullptr, EndStream::More);
+ }
+ if (end_stream_after_body_) {
+ // If we already reached the end of message and are still requesting more
+ // body, a null buffer indicates the body ended.
+ return absl::get(consumeCallback())(nullptr, EndStream::End);
+ }
+ // If we have no body or end but have requested some body, that means we're
+ // just waiting for it to arrive, and maybeDeliverBody will be called again
+ // when that happens.
+ return;
+ }
+ auto fragment = std::make_unique();
+ fragment->move(body_buffer_, len);
+ stream_pos_ += len;
+ stats().subUpstreamBufferedBytes(len);
+ bool end_stream = end_stream_after_body_ && body_buffer_.length() == 0;
+ return absl::get(consumeCallback())(
+ std::move(fragment), end_stream ? EndStream::End : EndStream::More);
+}
+
+void UpstreamRequestImpl::getTrailers(GetTrailersCallback&& cb) {
+ ASSERT(dispatcher_.isThreadSafe());
+ ASSERT(absl::holds_alternative(callback_));
+ ASSERT(!end_stream_after_headers_ && !end_stream_after_body_);
+ if (!stream_ && !trailers_) {
+ return cb(nullptr, EndStream::Reset);
+ }
+ callback_ = std::move(cb);
+ return maybeDeliverTrailers();
+}
+
+void UpstreamRequestImpl::onTrailers(Http::ResponseTrailerMapPtr&& trailers) {
+ ASSERT(dispatcher_.isThreadSafe());
+ trailers_ = std::move(trailers);
+ return maybeDeliverTrailers();
+}
+
+void UpstreamRequestImpl::maybeDeliverTrailers() {
+ ASSERT(dispatcher_.isThreadSafe());
+ if (!absl::holds_alternative(callback_) || !trailers_) {
+ if (body_buffer_.length() == 0 && absl::holds_alternative(callback_)) {
+ // If we received trailers while requesting body it means that we didn't
+ // know how much body to request, or the upstream returned less body than
+ // expected by surprise - a null body response informs the client to
+ // request trailers instead.
+ return absl::get(consumeCallback())(nullptr, EndStream::More);
+ }
+ return;
+ }
+ return absl::get(consumeCallback())(std::move(trailers_), EndStream::End);
+}
+
+UpstreamRequestImpl::~UpstreamRequestImpl() {
+ ASSERT(dispatcher_.isThreadSafe());
+ // Cancel in-flight callbacks on destroy.
+ callback_ = absl::monostate{};
+ cancel_();
+ if (stream_) {
+ // Resets the stream and calls onReset, guaranteeing no further callbacks.
+ stream_->reset();
+ }
+ if (body_buffer_.length() > 0) {
+ stats().subUpstreamBufferedBytes(body_buffer_.length());
+ }
+}
+
+void UpstreamRequestImpl::sendHeaders(Http::RequestHeaderMapPtr request_headers) {
+ ASSERT(dispatcher_.isThreadSafe());
+ // UpstreamRequest must take a copy of the headers as the AsyncStream may
+ // still use the reference provided to it after the original reference has moved.
+ request_headers_ = std::move(request_headers);
+ // If this request had a body or trailers, CacheFilter::decodeHeaders
+ // would have bypassed cache lookup and insertion, so this class wouldn't
+ // be instantiated. So end_stream will always be true.
+ stream_->sendHeaders(*request_headers_, /*end_stream=*/true);
+ absl::optional range_header = RangeUtils::getRangeHeader(*request_headers_);
+ if (range_header) {
+ absl::optional> ranges =
+ RangeUtils::parseRangeHeader(range_header.value(), 1);
+ if (ranges) {
+ stream_pos_ = ranges.value().front().firstBytePos();
+ }
+ }
+}
+
+template struct overloaded : Ts... {
+ using Ts::operator()...;
+};
+template overloaded(Ts...) -> overloaded;
+
+void UpstreamRequestImpl::onReset() {
+ ASSERT(dispatcher_.isThreadSafe());
+ stream_ = nullptr;
+ absl::visit(overloaded{
+ [](absl::monostate&&) {},
+ [](GetHeadersCallback&& cb) { cb(nullptr, EndStream::Reset); },
+ [](GetBodyCallback&& cb) { cb(nullptr, EndStream::Reset); },
+ [](GetTrailersCallback&& cb) { cb(nullptr, EndStream::Reset); },
+ },
+ consumeCallback());
+}
+
+void UpstreamRequestImpl::onComplete() {
+ ASSERT(dispatcher_.isThreadSafe());
+ stream_ = nullptr;
+}
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/filters/http/cache/upstream_request_impl.h b/source/extensions/filters/http/cache/upstream_request_impl.h
new file mode 100644
index 0000000000000..133a3b65d1a7f
--- /dev/null
+++ b/source/extensions/filters/http/cache/upstream_request_impl.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "source/common/buffer/watermark_buffer.h"
+#include "source/common/common/cancel_wrapper.h"
+#include "source/common/common/logger.h"
+#include "source/extensions/filters/http/cache/http_source.h"
+#include "source/extensions/filters/http/cache/range_utils.h"
+#include "source/extensions/filters/http/cache/stats.h"
+#include "source/extensions/filters/http/cache/upstream_request.h"
+
+#include "absl/types/variant.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+
+class UpstreamRequestImpl : public Logger::Loggable,
+ public UpstreamRequest,
+ public Http::AsyncClient::StreamCallbacks {
+public:
+ // Called from the factory.
+ void sendHeaders(Http::RequestHeaderMapPtr request_headers) override;
+ // HttpSource.
+ void getHeaders(GetHeadersCallback&& cb) override;
+ // Though range is an argument here, only the length is used by UpstreamRequest
+ // - the pieces requested should always be in order so we can just consume the
+ // stream as it comes.
+ void getBody(AdjustedByteRange range, GetBodyCallback&& cb) override;
+ void getTrailers(GetTrailersCallback&& cb) override;
+
+ // StreamCallbacks
+ void onHeaders(Http::ResponseHeaderMapPtr&& headers, bool end_stream) override;
+ void onData(Buffer::Instance& data, bool end_stream) override;
+ void onTrailers(Http::ResponseTrailerMapPtr&& trailers) override;
+ void onComplete() override;
+ void onReset() override;
+
+ // Called by WatermarkBuffer
+ void onAboveHighWatermark();
+ void onBelowLowWatermark();
+
+ ~UpstreamRequestImpl() override;
+
+private:
+ friend class UpstreamRequestImplFactory;
+ UpstreamRequestImpl(Event::Dispatcher& dispatcher, Http::AsyncClient& async_client,
+ const Http::AsyncClient::StreamOptions& options,
+ const std::shared_ptr stats_provider);
+ // If the headers and callback are both present, call the callback.
+ void maybeDeliverHeaders();
+
+ // If the required body chunk and callback are both present, call the callback.
+ void maybeDeliverBody();
+
+ // If the trailers and callback are both present, call the callback.
+ void maybeDeliverTrailers();
+
+ using CallbackTypes =
+ absl::variant;
+
+ // Returns the current callback and clears the member variable so it's safe to
+ // assert that it's empty.
+ CallbackTypes consumeCallback() { return std::exchange(callback_, absl::monostate{}); }
+
+ CacheFilterStats& stats() const { return stats_provider_->stats(); }
+
+ Event::Dispatcher& dispatcher_;
+ Http::AsyncClient::Stream* stream_;
+ Http::RequestHeaderMapPtr request_headers_;
+ Http::ResponseHeaderMapPtr headers_;
+ CallbackTypes callback_;
+ bool end_stream_after_headers_{false};
+ Buffer::WatermarkBuffer body_buffer_;
+ AdjustedByteRange requested_body_range_{0, 1};
+ uint64_t stream_pos_ = 0;
+ bool end_stream_after_body_{false};
+ Http::ResponseTrailerMapPtr trailers_;
+ CancelWrapper::CancelFunction cancel_ = []() {};
+ const std::shared_ptr stats_provider_;
+};
+
+class UpstreamRequestImplFactory : public UpstreamRequestFactory {
+public:
+ UpstreamRequestImplFactory(Event::Dispatcher& dispatcher, Http::AsyncClient& async_client,
+ Http::AsyncClient::StreamOptions stream_options)
+ : dispatcher_(dispatcher), async_client_(async_client),
+ stream_options_(std::move(stream_options)) {}
+
+ UpstreamRequestPtr
+ create(const std::shared_ptr stats_provider) override;
+
+private:
+ Event::Dispatcher& dispatcher_;
+ Http::AsyncClient& async_client_;
+ Http::AsyncClient::StreamOptions stream_options_;
+};
+
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/http/cache/file_system_http_cache/BUILD b/source/extensions/http/cache/file_system_http_cache/BUILD
index 00210bdd84017..9e9b573105f0b 100644
--- a/source/extensions/http/cache/file_system_http_cache/BUILD
+++ b/source/extensions/http/cache/file_system_http_cache/BUILD
@@ -20,6 +20,7 @@ envoy_cc_extension(
name = "config",
srcs = [
"cache_eviction_thread.cc",
+ "cache_file_reader.cc",
"config.cc",
"file_system_http_cache.cc",
"insert_context.cc",
@@ -28,6 +29,7 @@ envoy_cc_extension(
],
hdrs = [
"cache_eviction_thread.h",
+ "cache_file_reader.h",
"file_system_http_cache.h",
"insert_context.h",
"lookup_context.h",
@@ -48,6 +50,7 @@ envoy_cc_extension(
"//source/common/http:headers_lib",
"//source/common/protobuf",
"//source/extensions/common/async_files",
+ "//source/extensions/filters/http/cache:cache_sessions_impl_lib",
"//source/extensions/filters/http/cache:http_cache_lib",
"@com_google_absl//absl/base",
"@com_google_absl//absl/strings",
diff --git a/source/extensions/http/cache/file_system_http_cache/DESIGN.md b/source/extensions/http/cache/file_system_http_cache/DESIGN.md
index cd42187b849a6..760fe51a37899 100644
--- a/source/extensions/http/cache/file_system_http_cache/DESIGN.md
+++ b/source/extensions/http/cache/file_system_http_cache/DESIGN.md
@@ -9,12 +9,10 @@
- [ ] Eviction should be configurable as a "window", like watermarks, or with an optional frequency constraint, so the eviction thread can be kept from churning.
- [x] Cache should be limited to a specified amount of storage
- [ ] Cache should be configurable to periodically update the internal size from the filesystem, to account for external alterations.
-- [ ] Cache should mitigate thundering herd problem (i.e. if two or more workers request the same cacheable uncached result at the same time, only one worker should hit upstream). See [discussion](#thundering-herd).
- [ ] There should be an ability to remove objects from the cache with some kind of API call.
- [ ] Cache should expose counters for eviction stats (files evicted, bytes evicted).
- [ ] Cache should expose counters for timing information (eviction thread idle, eviction thread busy)
- [x] Cache should expose gauges for total size stored.
-- [ ] Cache should optionally expose histograms for insert and lookup latencies.
- [ ] Cache should optionally expose histogram for cache entry sizes.
- [x] Cache should index by the request route *and* a key generated from headers that may affect the outcome of a request (See [allowed_vary_headers](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/cache/v3/cache.proto.html))
- [ ] Cache should create a [tree structure](#tree-structure) of folders (may be configured as just one branch), so user may avoid filesystem performance issues with overcrowded directories.
@@ -22,35 +20,11 @@
## Storage design
-* The only state stored in memory is that a cache entry is in the process of being written; this allows other requests for the same resource in the same process to avoid creating duplicate write operations. (This is an optimization only - simultaneous writes don't break anything, and may occur when multiple processes are involved.)
+* A `CacheSession` maintains an open file handle of which ownership is passed to the `CacheSession`. It is possible for such an entry to be evicted (on a validation fail most likely), which should be fine - the file will be unlinked and the open file handle will keep the data "alive" until the requests using the old file handle are completed.
+* Simultaneous writes don't break anything, and may occur when multiple processes are touching the same cache.
* The cache can be configured with a maximum number of cache entry files, thereby effectively enforcing a maximum number of files per path.
* A new cache entry that causes the cache to exceed the configured maximum size or maximum number of entries triggers the eviction thread to evict sufficient LRU entries to bring it back below the threshold\[s\] exceeded.
-* Each cache entry file starts with [a fixed structure header followed by a serialized proto](cache_file_header.proto), followed by proto-serialized headers, raw body and proto-serialized trailers.
+* Each cache entry file starts with [a fixed structure header followed by a serialized proto](cache_file_header.proto), followed by raw body, proto-serialized trailers and proto-serialized headers. Headers are at the end to facilitate updating headers on validate operations.
* Cache entry files are named `cache-` followed by a stable hash key for the entry.
* (When implemented) the tree structure of folders is simply one level deep of folders named `cache-0000`, `cache-0001` etc. as four-digit hexadecimal numbers up to the configured number of subdirectories. Cache files are placed in a folder according to a short stable hash of their key. On cache startup, any cache entries found to be in the wrong folder (as would be the case if the number of folders was reconfigured) will simply be removed.
-
-## Discussions
-
-
-### Thundering herd
-
-The current implementation, if there are multiple requests for the same resource before the cache is populated, has only one of them perform an insert operation to the cache, and the rest simply bypass the cache. This can cause the "thundering herd" problem - if requests come in bursts the cache will not protect the upstream from that load.
-
-One possible solution would be to have all requesters for the same cache entry stream as the cache entry is written. However, if we do that, and the inserting stream gets closed prematurely, all the dependent streams would be forced to drop their also-incomplete responses.
-
-Another possible solution is to have secondary requesters wait until the cache entry is populated or abandoned before deciding whether to become cache readers or inserters (or bypass, if it turns out to be uncacheable). The downside of this option is that for large content, the dependent clients won't start streaming at all until the first client *finishes* streaming.
-
-An ideal solution would be to either make an inserting stream non-cancellable (i.e. if the client cancels, the upstream connection continues to stream to populate the cache). This could be achieved either by using the existing stream and adding a "non-cancellable" feature in the core (a bit of a large scale change), or by making insertion not use the existing stream at all, instead creating its own client. The problem with that option is that ideally the client would only pass through the filters that are upstream of the cache filter, and there is currently no mechanism for creating a new "partial filter chain" client like this.
-
-_Proposal from jmarantz:_
-
-The lock object could go into one of several states:
-* In-progress, size unknown
-* headers-complete, content length known and less than a threshold
-* headers-complete, content length known and more than a threshold
-* headers-complete, chunked encoding
-
-Each state could be individually configured as "block" or "pass through", allowing the user to decide which option is more appropriate for a particular use-case.
-
-This proposal would be redundant if we can figure a reliable way to stream a cache entry.
diff --git a/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.cc b/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.cc
index 8b3e43692f158..43b9445cb2c10 100644
--- a/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.cc
+++ b/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.cc
@@ -18,8 +18,8 @@ constexpr std::array ExpectedFileId = {'C', 'A', 'C', 'H'};
// The expected next four bytes of the header - if cacheVersionId() doesn't match
// ExpectedCacheVersionId then the file is from an incompatible cache version and should
// be removed from the cache.
-// Next 4 bytes of file should be "0000".
-constexpr std::array ExpectedCacheVersionId = {'0', '0', '0', '0'};
+// Next 4 bytes of file should be "0001".
+constexpr std::array ExpectedCacheVersionId = {'0', '0', '0', '1'};
} // namespace
diff --git a/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.h b/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.h
index cc675dc536bbd..0a4d0494ab1ee 100644
--- a/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.h
+++ b/source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.h
@@ -106,17 +106,11 @@ class CacheFileFixedBlock {
*/
void setTrailersSize(uint32_t sz) { trailer_size_ = sz; }
- /**
- * the offset from the start of the file to the start of the serialized headers proto.
- * @return the offset in bytes.
- */
- static uint32_t offsetToHeaders() { return size(); }
-
/**
* the offset from the start of the file to the start of the body data.
* @return the offset in bytes.
*/
- uint32_t offsetToBody() const { return offsetToHeaders() + headerSize(); }
+ static uint64_t offsetToBody() { return size(); }
/**
* the offset from the start of the file to the start of the serialized trailers proto.
@@ -125,10 +119,10 @@ class CacheFileFixedBlock {
uint64_t offsetToTrailers() const { return offsetToBody() + bodySize(); }
/**
- * the offset from the start of the file to the end of the file.
+ * the offset from the start of the file to the start of the serialized headers proto.
* @return the offset in bytes.
*/
- uint64_t offsetToEnd() const { return offsetToTrailers() + trailerSize(); }
+ uint64_t offsetToHeaders() const { return offsetToTrailers() + trailerSize(); }
/**
* is this a valid cache file header block for the current code version?
diff --git a/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.cc b/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.cc
index f150f6199bb16..e1938b259278f 100644
--- a/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.cc
+++ b/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.cc
@@ -91,6 +91,12 @@ CacheFileHeader makeCacheFileHeaderProto(Buffer::Instance& buffer) {
return ret;
}
+CacheFileTrailer makeCacheFileTrailerProto(Buffer::Instance& buffer) {
+ CacheFileTrailer ret;
+ ret.ParseFromString(buffer.toString());
+ return ret;
+}
+
} // namespace FileSystemHttpCache
} // namespace Cache
} // namespace HttpFilters
diff --git a/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.h b/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.h
index 8d003aee56a46..c1929d8e5158f 100644
--- a/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.h
+++ b/source/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util.h
@@ -97,6 +97,13 @@ ResponseMetadata metadataFromHeaderProto(const CacheFileHeader& header);
*/
CacheFileHeader makeCacheFileHeaderProto(Buffer::Instance& buffer);
+/**
+ * Deserializes a CacheFileTrailer message from a Buffer.
+ * @param buffer the buffer containing a serialized CacheFileTrailer message.
+ * @return the deserialized CacheFileTrailer message.
+ */
+CacheFileTrailer makeCacheFileTrailerProto(Buffer::Instance& buffer);
+
} // namespace FileSystemHttpCache
} // namespace Cache
} // namespace HttpFilters
diff --git a/source/extensions/http/cache/file_system_http_cache/cache_file_reader.cc b/source/extensions/http/cache/file_system_http_cache/cache_file_reader.cc
new file mode 100644
index 0000000000000..9c69c70eadb00
--- /dev/null
+++ b/source/extensions/http/cache/file_system_http_cache/cache_file_reader.cc
@@ -0,0 +1,41 @@
+#include "source/extensions/http/cache/file_system_http_cache/cache_file_reader.h"
+
+#include "source/extensions/http/cache/file_system_http_cache/cache_file_fixed_block.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+namespace FileSystemHttpCache {
+
+using Common::AsyncFiles::AsyncFileHandle;
+
+CacheFileReader::CacheFileReader(AsyncFileHandle handle) : file_handle_(handle) {}
+
+void CacheFileReader::getBody(Event::Dispatcher& dispatcher, AdjustedByteRange range,
+ GetBodyCallback&& cb) {
+ auto queued = file_handle_->read(
+ &dispatcher, CacheFileFixedBlock::offsetToBody() + range.begin(), range.length(),
+ [len = range.length(),
+ cb = std::move(cb)](absl::StatusOr read_result) mutable -> void {
+ if (!read_result.ok()) {
+ return cb(nullptr, EndStream::Reset);
+ }
+ if (read_result.value()->length() != len) {
+ return cb(nullptr, EndStream::Reset);
+ }
+ return cb(std::move(read_result.value()), EndStream::More);
+ });
+ ASSERT(queued.ok(), queued.status().ToString());
+}
+
+CacheFileReader::~CacheFileReader() {
+ auto queued = file_handle_->close(nullptr, [](absl::Status) {});
+ ASSERT(queued.ok());
+}
+
+} // namespace FileSystemHttpCache
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/http/cache/file_system_http_cache/cache_file_reader.h b/source/extensions/http/cache/file_system_http_cache/cache_file_reader.h
new file mode 100644
index 0000000000000..ec519e8a81553
--- /dev/null
+++ b/source/extensions/http/cache/file_system_http_cache/cache_file_reader.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "source/extensions/common/async_files/async_file_handle.h"
+#include "source/extensions/filters/http/cache/http_cache.h"
+
+namespace Envoy {
+namespace Extensions {
+namespace HttpFilters {
+namespace Cache {
+namespace FileSystemHttpCache {
+
+class CacheFileReader : public CacheReader {
+public:
+ CacheFileReader(Common::AsyncFiles::AsyncFileHandle handle);
+ ~CacheFileReader() override;
+ // From CacheReader
+ void getBody(Event::Dispatcher& dispatcher, AdjustedByteRange range, GetBodyCallback&& cb) final;
+
+private:
+ Common::AsyncFiles::AsyncFileHandle file_handle_;
+};
+
+} // namespace FileSystemHttpCache
+} // namespace Cache
+} // namespace HttpFilters
+} // namespace Extensions
+} // namespace Envoy
diff --git a/source/extensions/http/cache/file_system_http_cache/config.cc b/source/extensions/http/cache/file_system_http_cache/config.cc
index 6e80cc217419a..f065116e6eb55 100644
--- a/source/extensions/http/cache/file_system_http_cache/config.cc
+++ b/source/extensions/http/cache/file_system_http_cache/config.cc
@@ -6,6 +6,7 @@
#include "envoy/registry/registry.h"
#include "source/extensions/common/async_files/async_file_manager_factory.h"
+#include "source/extensions/filters/http/cache/cache_sessions.h"
#include "source/extensions/filters/http/cache/http_cache.h"
#include "source/extensions/http/cache/file_system_http_cache/cache_eviction_thread.h"
#include "source/extensions/http/cache/file_system_http_cache/file_system_http_cache.h"
@@ -47,10 +48,10 @@ class CacheSingleton : public Envoy::Singleton::Instance {
: async_file_manager_factory_(async_file_manager_factory),
cache_eviction_thread_(thread_factory) {}
- std::shared_ptr get(std::shared_ptr singleton,
- const ConfigProto& non_normalized_config,
- Stats::Scope& stats_scope) {
- std::shared_ptr cache;
+ std::shared_ptr get(std::shared_ptr singleton,
+ const ConfigProto& non_normalized_config,
+ Server::Configuration::FactoryContext& context) {
+ std::shared_ptr cache;
ConfigProto config = normalizeConfig(non_normalized_config);
auto key = config.cache_path();
absl::MutexLock lock(&mu_);
@@ -61,14 +62,20 @@ class CacheSingleton : public Envoy::Singleton::Instance {
if (!cache) {
std::shared_ptr async_file_manager =
async_file_manager_factory_->getAsyncFileManager(config.manager_config());
- cache = std::make_shared(singleton, cache_eviction_thread_,
- std::move(config),
- std::move(async_file_manager), stats_scope);
+ std::unique_ptr fs_cache = std::make_unique(
+ singleton, cache_eviction_thread_, std::move(config), std::move(async_file_manager),
+ context.scope());
+ cache = CacheSessions::create(context, std::move(fs_cache));
caches_[key] = cache;
- } else if (!Protobuf::util::MessageDifferencer::Equals(cache->config(), config)) {
- throw EnvoyException(
- fmt::format("mismatched FileSystemHttpCacheConfig with same path\n{}\nvs.\n{}",
- cache->config().DebugString(), config.DebugString()));
+ } else {
+ // Check that the config of the cache found in the lookup table for the given path
+ // has the same config as the config being added.
+ FileSystemHttpCache& fs_cache = static_cast(cache->cache());
+ if (!Protobuf::util::MessageDifferencer::Equals(fs_cache.config(), config)) {
+ throw EnvoyException(
+ fmt::format("mismatched FileSystemHttpCacheConfig with same path\n{}\nvs.\n{}",
+ fs_cache.config().DebugString(), config.DebugString()));
+ }
}
return cache;
}
@@ -81,7 +88,7 @@ class CacheSingleton : public Envoy::Singleton::Instance {
// that config of cache. The caches each keep shared_ptrs to this singleton, which keeps the
// singleton from being destroyed unless it's no longer keeping track of any caches.
// (The singleton shared_ptr is *only* held by cache instances.)
- absl::flat_hash_map> caches_ ABSL_GUARDED_BY(mu_);
+ absl::flat_hash_map> caches_ ABSL_GUARDED_BY(mu_);
};
SINGLETON_MANAGER_REGISTRATION(file_system_http_cache_singleton);
@@ -95,7 +102,7 @@ class FileSystemHttpCacheFactory : public HttpCacheFactory {
return std::make_unique();
}
// From HttpCacheFactory
- std::shared_ptr
+ std::shared_ptr
getCache(const envoy::extensions::filters::http::cache::v3::CacheConfig& filter_config,
Server::Configuration::FactoryContext& context) override {
ConfigProto config;
@@ -108,7 +115,7 @@ class FileSystemHttpCacheFactory : public HttpCacheFactory {
&context.serverFactoryContext().singletonManager()),
context.serverFactoryContext().api().threadFactory());
});
- return caches->get(caches, config, context.scope());
+ return caches->get(caches, config, context);
}
};
diff --git a/source/extensions/http/cache/file_system_http_cache/file_system_http_cache.cc b/source/extensions/http/cache/file_system_http_cache/file_system_http_cache.cc
index 6497a941f8fa7..231843b0a097f 100644
--- a/source/extensions/http/cache/file_system_http_cache/file_system_http_cache.cc
+++ b/source/extensions/http/cache/file_system_http_cache/file_system_http_cache.cc
@@ -17,62 +17,9 @@ namespace HttpFilters {
namespace Cache {
namespace FileSystemHttpCache {
-// Copying in 128K chunks is an arbitrary choice for a reasonable balance of performance and
-// memory usage. Since UpdateHeaders is unlikely to be a common operation it is most likely
-// not worthwhile to carefully tune this.
-const size_t FileSystemHttpCache::max_update_headers_copy_chunk_size_ = 128 * 1024;
-
const CacheStats& FileSystemHttpCache::stats() const { return shared_->stats_; }
const ConfigProto& FileSystemHttpCache::config() const { return shared_->config_; }
-void FileSystemHttpCache::writeVaryNodeToDisk(Event::Dispatcher& dispatcher, const Key& key,
- const Http::ResponseHeaderMap& response_headers,
- std::shared_ptr cleanup) {
- auto vary_values = VaryHeaderUtils::getVaryValues(response_headers);
- auto headers = std::make_shared();
- auto h = headers->add_headers();
- h->set_key("vary");
- h->set_value(absl::StrJoin(vary_values, ","));
- std::string filename = absl::StrCat(cachePath(), generateFilename(key));
- async_file_manager_->createAnonymousFile(
- &dispatcher, cachePath(),
- [headers, filename = std::move(filename), cleanup,
- dispatcher = &dispatcher](absl::StatusOr open_result) {
- if (!open_result.ok()) {
- ENVOY_LOG(warn, "writing vary node, failed to createAnonymousFile: {}",
- open_result.status());
- return;
- }
- auto file_handle = std::move(open_result.value());
- CacheFileFixedBlock block;
- auto buf = bufferFromProto(*headers);
- block.setHeadersSize(buf.length());
- Buffer::OwnedImpl buf2;
- block.serializeToBuffer(buf2);
- buf2.add(buf);
- size_t sz = buf2.length();
- auto queued = file_handle->write(
- dispatcher, buf2, 0,
- [dispatcher, file_handle, cleanup, sz,
- filename = std::move(filename)](absl::StatusOr write_result) {
- if (!write_result.ok() || write_result.value() != sz) {
- ENVOY_LOG(warn, "writing vary node, failed to write: {}", write_result.status());
- file_handle->close(nullptr, [](absl::Status) {}).IgnoreError();
- return;
- }
- auto queued = file_handle->createHardLink(
- dispatcher, filename, [cleanup, file_handle](absl::Status link_result) {
- if (!link_result.ok()) {
- ENVOY_LOG(warn, "writing vary node, failed to link: {}", link_result);
- }
- file_handle->close(nullptr, [](absl::Status) {}).IgnoreError();
- });
- ASSERT(queued.ok());
- });
- ASSERT(queued.ok());
- });
-}
-
absl::string_view FileSystemHttpCache::name() {
return "envoy.extensions.http.cache.file_system_http_cache";
}
@@ -82,303 +29,243 @@ FileSystemHttpCache::FileSystemHttpCache(
ConfigProto config, std::shared_ptr&& async_file_manager,
Stats::Scope& stats_scope)
: owner_(owner), async_file_manager_(async_file_manager),
- shared_(std::make_shared(config, stats_scope)),
- cache_eviction_thread_(cache_eviction_thread) {
+ shared_(std::make_shared