From 3ff732d4ca3a538c7ebfb40a9ab6255297aabf01 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Thu, 26 Mar 2020 16:08:30 +0300 Subject: [PATCH 01/33] Add HazelcastHttpCache Signed-off-by: Enes Ozcan --- bazel/foreign_cc/BUILD | 14 + bazel/foreign_cc/hazelcast_cpp_client.patch | 12 + bazel/repositories.bzl | 15 + bazel/repository_locations.bzl | 7 + source/common/common/logger.h | 1 + source/extensions/extensions_build_config.bzl | 1 + .../http/cache/hazelcast_http_cache/BUILD | 37 ++ .../cache/hazelcast_http_cache/config.proto | 46 ++ .../cache/hazelcast_http_cache/config_util.h | 52 +++ .../hazelcast_cache_entry.cc | 134 ++++++ .../hazelcast_cache_entry.h | 196 ++++++++ .../hazelcast_http_cache/hazelcast_context.cc | 354 +++++++++++++++ .../hazelcast_http_cache/hazelcast_context.h | 207 +++++++++ .../hazelcast_http_cache.cc | 235 ++++++++++ .../hazelcast_http_cache.h | 129 ++++++ .../http/cache/hazelcast_http_cache/BUILD | 41 ++ .../hazelcast_divided_cache_test.cc | 419 ++++++++++++++++++ .../hazelcast_unified_cache_test.cc | 321 ++++++++++++++ .../http/cache/hazelcast_http_cache/util.h | 198 +++++++++ 19 files changed, 2419 insertions(+) create mode 100644 bazel/foreign_cc/hazelcast_cpp_client.patch create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/BUILD create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/config.proto create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/BUILD create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/util.h diff --git a/bazel/foreign_cc/BUILD b/bazel/foreign_cc/BUILD index 24910612adf22..5f7e7dc355725 100644 --- a/bazel/foreign_cc/BUILD +++ b/bazel/foreign_cc/BUILD @@ -176,6 +176,20 @@ envoy_cmake_external( }), ) +envoy_cmake_external( + name = "hazelcast_cpp_client", + cache_entries = { + "HZ_LIB_TYPE": "STATIC", + "CMAKE_BUILD_TYPE": "RELEASE", + "CMAKE_CXX_FLAGS" : "-Wno-deprecated", + }, + lib_source = "@com_github_hazelcast_cpp_client//:all", + static_libraries = select({ + "//bazel:windows_x86_64": ["libHazelcastClient3.12.1_64.lib"], + "//conditions:default": ["libHazelcastClient3.12.1_64.a"], + }), +) + envoy_cmake_external( name = "nghttp2", cache_entries = { diff --git a/bazel/foreign_cc/hazelcast_cpp_client.patch b/bazel/foreign_cc/hazelcast_cpp_client.patch new file mode 100644 index 0000000000000..0d94dd46b7900 --- /dev/null +++ b/bazel/foreign_cc/hazelcast_cpp_client.patch @@ -0,0 +1,12 @@ +--- CMakeLists.txt 2020-02-27 11:35:35.000000000 +0300 ++++ CMakeLists.txt 2020-02-27 11:37:15.000000000 +0300 +@@ -279,3 +279,9 @@ + ADD_SUBDIRECTORY(examples) + message(STATUS "Configured to build the examples.") + ENDIF(HZ_BUILD_EXAMPLES) ++INSTALL(TARGETS ${HZ_LIB_NAME} DESTINATION lib) ++INSTALL(DIRECTORY ${PROJECT_SOURCE_DIR}/hazelcast/generated-sources/include/hazelcast DESTINATION include FILES_MATCHING PATTERN "*.h") ++INSTALL(DIRECTORY ${PROJECT_SOURCE_DIR}/hazelcast/include/hazelcast DESTINATION include FILES_MATCHING PATTERN "*.h" PATTERN "*.inl") ++INSTALL(DIRECTORY ${PROJECT_SOURCE_DIR}/external/release_include/boost DESTINATION include FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") ++INSTALL(DIRECTORY ${PROJECT_SOURCE_DIR}/external/release_include/easylogging++ DESTINATION include FILES_MATCHING PATTERN "*.h") ++INSTALL(DIRECTORY ${PROJECT_SOURCE_DIR}/external/include/asio/asio/include/asio DESTINATION include FILES_MATCHING PATTERN "*.hpp") diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index d2bbc33309619..ff9553034ecae 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -128,6 +128,7 @@ def envoy_dependencies(skip_targets = []): _com_github_google_libprotobuf_mutator() _com_github_gperftools_gperftools() _com_github_grpc_grpc() + _com_github_hazelcast_cpp_client() _com_github_jbeder_yaml_cpp() _com_github_libevent_libevent() _com_github_luajit_luajit() @@ -289,6 +290,20 @@ def _com_github_google_libprotobuf_mutator(): build_file = "@envoy//bazel/external:libprotobuf_mutator.BUILD", ) +def _com_github_hazelcast_cpp_client(): + location = REPOSITORY_LOCATIONS["com_github_hazelcast_cpp_client"] + http_archive( + name = "com_github_hazelcast_cpp_client", + build_file_content = BUILD_ALL_CONTENT, + patch_args = ["-p0"], + patches = ["@envoy//bazel/foreign_cc:hazelcast_cpp_client.patch"], + **location + ) + native.bind( + name = "hazelcast_cpp_client", + actual = "@envoy//bazel/foreign_cc:hazelcast_cpp_client", + ) + def _com_github_jbeder_yaml_cpp(): location = REPOSITORY_LOCATIONS["com_github_jbeder_yaml_cpp"] http_archive( diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 0465bef4469f4..f1652463e35c6 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -115,6 +115,13 @@ REPOSITORY_LOCATIONS = dict( strip_prefix = "grpc-d8f4928fa779f6005a7fe55a176bdb373b0f910f", urls = ["https://github.com/grpc/grpc/archive/d8f4928fa779f6005a7fe55a176bdb373b0f910f.tar.gz"], ), + com_github_hazelcast_cpp_client = dict( + sha256 = "3c43c81135e415ce708486564dc125bde93c2c9f8965d5af4b603ec91ff52f6e", + strip_prefix = "hazelcast-cpp-client-3.12.1", + # Using non official tarball due to missing submodule files in the official release. + # TODO(enozcan): Official tar can be used with a solution to init&update submodules + urls = ["https://github.com/enozcan/envoy/raw/hazelcast_tarball/hazelcast-cpp-client-3.12.1.zip"], + ), com_github_luajit_luajit = dict( sha256 = "409f7fe570d3c16558e594421c47bdd130238323c9d6fd6c83dedd2aaeb082a8", strip_prefix = "LuaJIT-2.1.0-beta3", diff --git a/source/common/common/logger.h b/source/common/common/logger.h index 324d08e476a28..67263a16511d0 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -38,6 +38,7 @@ namespace Logger { FUNCTION(filter) \ FUNCTION(forward_proxy) \ FUNCTION(grpc) \ + FUNCTION(hazelcast_http_cache) \ FUNCTION(hc) \ FUNCTION(health_checker) \ FUNCTION(http) \ diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 3c85a8a61a6af..696ce063a36ea 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -167,6 +167,7 @@ EXTENSIONS = { # CacheFilter plugins # + "envoy.filters.http.cache.hazelcast_http_cache": "//source/extensions/filters/http/cache/hazelcast_http_cache:hazelcast_http_cache_lib", "envoy.filters.http.cache.simple_http_cache": "//source/extensions/filters/http/cache/simple_http_cache:simple_http_cache_lib", } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD new file mode 100644 index 0000000000000..ea9573a3fd082 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -0,0 +1,37 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_proto_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_extension( + name = "hazelcast_http_cache_lib", + srcs = ["hazelcast_http_cache.cc", + "hazelcast_cache_entry.cc", + "hazelcast_context.cc"], + hdrs = ["hazelcast_http_cache.h", + "hazelcast_cache_entry.h", + "hazelcast_context.h", + "config_util.h"], + status = "wip", + security_posture = "robust_to_untrusted_downstream_and_upstream", + external_deps = ["hazelcast_cpp_client"], + deps = [ + ":config_cc_proto", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + "//source/common/http:header_map_lib", + "//include/envoy/registry", + "//source/extensions/filters/http/cache:http_cache_lib", + ], +) + +envoy_proto_library( + name = "config", + srcs = ["config.proto"], +) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto new file mode 100644 index 0000000000000..a2fcfc4c2349f --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +package envoy.source.extensions.filters.http.cache; + +// [#protodoc-title: HazelcastHttpCache CacheFilter storage plugin] +// CacheFilter plugin backed by Hazelcast IMDG. +// [#extension: envoy.extensions.http.cache] + +// Hazelcast Http Cache configuration +// TODO: Mention defaults. +message HazelcastHttpCacheConfig { + // Group name of Hazelcast cluster to be connected. + string group_name = 1; + // Group password of Hazelcast cluster to be connected. + string group_password = 2; + + // IP address of Hazelcast member to be connected. + string ip = 3; + // Port of Hazelcast member to be connected. + int32 port = 4; + + // Application specific name for the cache. Different deployments should + // use the same prefix if they want to share the same cache and connect + // to the same Hazelcast cluster. + string app_prefix = 5; + + // In unified mode, cached responses will be stored as a single entry. + // On a range HTTP request, regardless of the request range, all the + // body will be called from the remote cache and then the requested + // range will be served. + // In divided mode, cached responses will be stored in two different + // maps: header map and body map. For a response to be cached, its + // header is stored in the header map and its body is stored in body + // map partitioned with body_partition_size. On a range request, only + // required body partitions are called from the distributed map. This + // option causes extra memory usage per partition on the cache. + bool unified = 6; + + // Body partition size for divided cache. Ignored in unified mode. + int64 body_partition_size = 7; + + // Maximum allowed body size per response. If insertion for a larger + // value than the limit is attempted, the first max_body_size bytes + // of the response will be cached and the remaining will be ignored. + int64 max_body_size = 8; +} diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h new file mode 100644 index 0000000000000..e2f40a8074161 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h @@ -0,0 +1,52 @@ +#pragma once + +#include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" +#include "hazelcast/client/ClientConfig.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +class ConfigUtil { +public: + + static uint64_t validPartitionSize(const uint64_t& config_value) { + return config_value == 0 ? + DEFAULT_PARTITION_SIZE : (config_value > MAX_PARTITION_SIZE) ? + MAX_PARTITION_SIZE : config_value; + } + + static uint64_t validMaxBodySize(const uint64_t& config_value) { + return config_value == 0 || (config_value > MAX_BODY_SIZE) ? + MAX_BODY_SIZE : config_value; + } + + static hazelcast::client::ClientConfig getClientConfig(const + envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig& cache_config) { + // TODO: Add retry config, connection config. Use multiple addresses. + hazelcast::client::ClientConfig config; + config.getGroupConfig().setName(cache_config.group_name()); + config.getNetworkConfig().addAddress(hazelcast::client::Address(cache_config.ip(), + cache_config.port())); + return config; + } + + static short partitionWarnLimit() { + return WARN_PARTITION_LIMIT; + } + +private: + // TODO: Examine the optimal values for defaults and limits. + static constexpr short WARN_PARTITION_LIMIT = 20; + static constexpr uint64_t DEFAULT_PARTITION_SIZE = 4096; + static constexpr uint64_t MAX_PARTITION_SIZE = DEFAULT_PARTITION_SIZE * 16; + static constexpr uint64_t MAX_BODY_SIZE = MAX_PARTITION_SIZE * 16; +}; + +} // HazelcastHttpCache +} // Cache +} // HttpFilters +} // Extensions +} // Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc new file mode 100644 index 0000000000000..c21101104d71a --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc @@ -0,0 +1,134 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +void HazelcastHeaderEntry::writeData(ObjectDataOutput& writer) const { + // Serialization of HeaderEntry to be written to distributed map. + // Order of reading must be the same with this. + writeUnifiedData(writer); + writer.writeInt(version_); + + // Hazelcast stores signed types only. + // Casting to signed before writing and back to unsigned when reading. + writer.writeLong(static_cast(body_size_)); +} + +void HazelcastHeaderEntry::readData(ObjectDataInput& reader) { + // Deserialization of HeaderEntry to be read from distributed map. + // Order of writing must be the same with this. + readUnifiedData(reader); + version_ = reader.readInt(); + int64_t signed_size = reader.readLong(); + std::memcpy(&body_size_, &signed_size, sizeof(uint64_t)); +} + +void HazelcastHeaderEntry::writeUnifiedData(ObjectDataOutput& writer) const { + writer.writeInt(header_map_->size()); + header_map_->iterate( + [](const Http::HeaderEntry& header, void* context) -> Http::HeaderMap::Iterate { + ObjectDataOutput* writer = static_cast(context); + absl::string_view key_view = header.key().getStringView(); + absl::string_view val_view = header.value().getStringView(); + std::vector key_vector(key_view.begin(), key_view.end()); + std::vector val_vector(val_view.begin(), val_view.end()); + writer->writeCharArray(&key_vector); + writer->writeCharArray(&val_vector); + return Http::HeaderMap::Iterate::Continue; + }, + &writer); + std::string serialized; + variant_key_.SerializeToString(&serialized); + writer.writeUTF(&serialized); +} + +void HazelcastHeaderEntry::readUnifiedData(ObjectDataInput& reader) { + int headers_size = reader.readInt(); + header_map_ = std::make_unique(); + for (int i = 0; i < headers_size; i++) { + std::vector key_vector = *reader.readCharArray(); + std::vector val_vector = *reader.readCharArray(); + Http::HeaderString key, val; + key.append(key_vector.data(), key_vector.size()); + val.append(val_vector.data(), val_vector.size()); + header_map_->addViaMove(std::move(key), std::move(val)); + } + std::string serialized = *reader.readUTF(); + variant_key_.ParseFromString(serialized); +} + +HazelcastHeaderEntry::HazelcastHeaderEntry() = default; + +HazelcastHeaderEntry::HazelcastHeaderEntry(Http::ResponseHeaderMapPtr&& header_map, Key&& key, + uint64_t body_size, int32_t version) : header_map_(std::move(header_map)), + variant_key_(std::move(key)), body_size_(body_size), version_(version) {} + +HazelcastHeaderEntry::HazelcastHeaderEntry(const HazelcastHeaderEntry& other) { + body_size_ = other.body_size_; + variant_key_ = other.variant_key_; + header_map_ = Http::createHeaderMap(*other.header_map_); + version_ = other.version_; +} + +HazelcastHeaderEntry::HazelcastHeaderEntry(HazelcastHeaderEntry&& other) : + header_map_(std::move(other.header_map_)), variant_key_(std::move(other.variant_key_)), + body_size_(other.body_size_), version_(other.version_) {} + +void HazelcastBodyEntry::writeData(ObjectDataOutput& writer) const { + writeUnifiedData(writer); + writer.writeInt(version_); +} + +void HazelcastBodyEntry::readData(ObjectDataInput& reader) { + readUnifiedData(reader); + version_ = reader.readInt(); +} + +void HazelcastBodyEntry::writeUnifiedData(ObjectDataOutput& writer) const { + writer.writeByteArray(&body_buffer_); +} + +void HazelcastBodyEntry::readUnifiedData(ObjectDataInput& reader) { + body_buffer_ = *reader.readByteArray(); +} + +HazelcastBodyEntry::HazelcastBodyEntry() = default; + +HazelcastBodyEntry::HazelcastBodyEntry(int64_t header_key, + std::vector&& buffer, int32_t version) : header_key_(header_key), + version_(version), body_buffer_(std::move(buffer)) {} + +HazelcastBodyEntry::HazelcastBodyEntry(const HazelcastBodyEntry& other) { + body_buffer_ = other.body_buffer_; + header_key_ = other.header_key_; + version_ = other.version_; +} + +HazelcastBodyEntry::HazelcastBodyEntry(HazelcastBodyEntry&& other) : + header_key_(other.header_key_), version_(other.version_), + body_buffer_(std::move(other.body_buffer_)) {} + +void HazelcastResponseEntry::writeData(ObjectDataOutput& writer) const { + response_header_.writeUnifiedData(writer); + response_body_.writeUnifiedData(writer); +} + +void HazelcastResponseEntry::readData(ObjectDataInput& reader) { + response_header_.readUnifiedData(reader); + response_body_.readUnifiedData(reader); +} + +HazelcastResponseEntry::HazelcastResponseEntry() = default; + +HazelcastResponseEntry::HazelcastResponseEntry(HazelcastHeaderEntry&& header, + HazelcastBodyEntry&& body) : response_header_(std::move(header)), + response_body_(std::move(body)) {}; + +} // HazelcastHttpCache +} // Cache +} // HttpFilters +} // Extensions +} // Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h new file mode 100644 index 0000000000000..c53be18ca0f93 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h @@ -0,0 +1,196 @@ +#pragma once + +#include "common/http/header_map_impl.h" + +#include "hazelcast/client/serialization/IdentifiedDataSerializable.h" +#include "hazelcast/client/serialization/ObjectDataInput.h" +#include "hazelcast/client/serialization/ObjectDataOutput.h" +#include "hazelcast/client/PartitionAware.h" + +#include "source/extensions/filters/http/cache/key.pb.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +using hazelcast::client::PartitionAware; +using hazelcast::client::serialization::IdentifiedDataSerializable; +using hazelcast::client::serialization::ObjectDataOutput; +using hazelcast::client::serialization::ObjectDataInput; +using hazelcast::client::serialization::DataSerializableFactory; + +static const int HAZELCAST_BODY_TYPE_ID = 100; +static const int HAZELCAST_HEADER_TYPE_ID = 101; +static const int HAZELCAST_RESPONSE_TYPE_ID = 102; +static const int HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID = 1000; + +/** + * Response header wrapper for cache entries. + * + * @note In DIVIDED cache mode, response headers and corresponding bodies will be + * stored in different distributed maps. This option is in favor of the efficiency + * of range http requests. For each header entry, there will be body entries (if any) + * with a relevant key. + */ +class HazelcastHeaderEntry : public IdentifiedDataSerializable { +public: + HazelcastHeaderEntry(); + HazelcastHeaderEntry(Http::ResponseHeaderMapPtr&& header_map, Key&& key, uint64_t body_size, + int32_t version); + HazelcastHeaderEntry(const HazelcastHeaderEntry& other); + HazelcastHeaderEntry(HazelcastHeaderEntry&& other); + + // hazelcast::client::serialization::IdentifiedDataSerializable + void writeData(ObjectDataOutput& writer) const; + void readData(ObjectDataInput& reader); + + // Only required fields of a header entry for unified mode are + // de/serialized in unifiedData methods. + void writeUnifiedData(ObjectDataOutput& writer) const; + void readUnifiedData(ObjectDataInput& reader); + + const Key& variantKey() const { return variant_key_; } + const uint64_t& bodySize() const { return body_size_; } + const int32_t& version() const { return version_; } + Http::ResponseHeaderMapPtr& headerMap() { return header_map_; } + + void variantKey(Key&& key) { variant_key_ = std::move(key); } + void headerMap(Http::ResponseHeaderMapPtr&& header_map) { header_map_ = std::move(header_map); } + void bodySize(uint64_t body_size) { body_size_ = body_size; } + +private: + // hazelcast::client::serialization::IdentifiedDataSerializable + int getClassId() const { return HAZELCAST_HEADER_TYPE_ID; } + int getFactoryId() const { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + + Http::ResponseHeaderMapPtr header_map_; + + /** Key generated by the cache filter and modified with vary headers later on. */ + Key variant_key_; + + /** Total body size of the response with these headers. */ + uint64_t body_size_; + + /** Marker to link bodies to header. Bodies in DIVIDED mode will have the same + * version and hence it is ensured a body partition belongs to the correct header. + * Used to handle malformed responses. */ + int32_t version_; +}; + +/** +* Response body wrapper for cache entries. +* +* @note In DIVIDED cache mode, response headers and corresponding bodies will be stored in +* different distributed maps. For a response HeaderEntry with 64 bit hash key , bodies +* will be stored with keys "0", "1", "2".. and so on in a contiguous manner. +* Body partition size is fixed and configurable via cache config. On a range request, only +* necessary partitions according to the request will be fetched from distributed map, +* not the whole response. +*/ +class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAware { +public: + HazelcastBodyEntry(); + HazelcastBodyEntry(int64_t header_key, std::vector&& buffer, int32_t version); + HazelcastBodyEntry(const HazelcastBodyEntry& other); + HazelcastBodyEntry(HazelcastBodyEntry&& other); + + // hazelcast::client::serialization::IdentifiedDataSerializable + void writeData(ObjectDataOutput& writer) const; + void readData(ObjectDataInput& reader); + + // Only required fields of a body entry for unified mode are + // de/serialized in unifiedData methods. + void writeUnifiedData(ObjectDataOutput& writer) const; + void readUnifiedData(ObjectDataInput& reader); + + size_t length() const { return body_buffer_.size(); } + hazelcast::byte* begin() { return body_buffer_.data(); } + const int32_t& version() const { return version_; } + + void bodyBuffer(std::vector&& buffer) { body_buffer_ = std::move(buffer); } + void headerKey(int64_t key) { header_key_ = key; } + void version(int32_t version) { version_ = version; } + +private: + // hazelcast::client::serialization::IdentifiedDataSerializable + int getClassId() const { return HAZELCAST_BODY_TYPE_ID; } + int getFactoryId() const { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + + // hazelcast::client::PartitionAware + const int64_t* getPartitionKey() const { return &header_key_; } + + /** The same hash key with the corresponding header. */ + // Not stored in distributed map but used to store related bodies in the same + // node in Hazelcast cluster. Hence (possible) extra networking calls are prevented. + int64_t header_key_; + + /** Derived from header. */ + int32_t version_; + + std::vector body_buffer_; +}; + +/** +* Response wrapper for cache entries. +* +* @note In UNIFIED cache mode, unlike DIVIDED, there is only one cache entry containing +* the response as a whole. Even if a range request arrives, all the body is fetched from +* the cache. This option is in favor of the efficiency of http responses with small body +* sizes. Hence it prevents extra calls for bodies after fetching header. +*/ +class HazelcastResponseEntry : public IdentifiedDataSerializable { +public: + HazelcastResponseEntry(); + HazelcastResponseEntry(HazelcastHeaderEntry&& header, HazelcastBodyEntry&& body); + + HazelcastHeaderEntry& header() { return response_header_; } + HazelcastBodyEntry& body() { return response_body_; } + +private: + // hazelcast::client::serialization::IdentifiedDataSerializable + void writeData(ObjectDataOutput& writer) const; + void readData(ObjectDataInput& reader); + int getClassId() const { return HAZELCAST_RESPONSE_TYPE_ID; } + int getFactoryId() const { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + + HazelcastHeaderEntry response_header_; + HazelcastBodyEntry response_body_; +}; + +// To make cache compatible with Hazelcast Cpp Client, boost pointers are +// used internally instead of std. +using HazelcastHeaderPtr = boost::shared_ptr; +using HazelcastBodyPtr = boost::shared_ptr; +using HazelcastResponsePtr = boost::shared_ptr; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +class HazelcastCacheEntrySerializableFactory : public DataSerializableFactory { + +public: + static const int FACTORY_ID = HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; + + virtual std::auto_ptr create(int32_t classId) { + switch (classId) { + case HAZELCAST_BODY_TYPE_ID: + return std::auto_ptr(new HazelcastBodyEntry()); + case HAZELCAST_HEADER_TYPE_ID: + return std::auto_ptr(new HazelcastHeaderEntry()); + case HAZELCAST_RESPONSE_TYPE_ID: + return std::auto_ptr(new HazelcastResponseEntry()); + default: + return std::auto_ptr(); + } + } +}; + +#pragma GCC diagnostic pop + +} // HazelcastHttpCache +} // Cache +} // HttpFilters +} // Extensions +} // Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc new file mode 100644 index 0000000000000..da5bd7b5e4bbe --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -0,0 +1,354 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/config_util.h" + +#include "common/buffer/buffer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +using Envoy::Protobuf::util::MessageDifferencer; + +UnifiedLookupContext::UnifiedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request) + : HazelcastLookupContextBase(cache, std::move(request)) {} + +void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { + ENVOY_LOG(debug, "Looking up unified response with key {}u", variant_hash_key_); + try { + response_ = hz_cache_.getResponse(variant_hash_key_); + } catch (HazelcastClientOfflineException e){ + ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" + "until the connection is restored..."); + abort_insertion_ = true; + cb(LookupResult{}); + return; + } + if (response_) { + ENVOY_LOG(debug, "Found unified response for key {}u, " + "body size = {}", variant_hash_key_, response_->body().length()); + if (!MessageDifferencer::Equals(response_->header().variantKey(), variantKey())) { + // As cache filter denotes, a secondary check other than the hash key + // is performed here. If a different response is found with the same + // hash (probably on hash collisions), the new response is denied to + // be cached and the old one remains. + ENVOY_LOG(debug, "Keys mismatched for hash {}u. " + "Aborting lookup & insertion", variant_hash_key_); + abort_insertion_ = true; + cb(LookupResult{}); + return; + } + cb(lookup_request_.makeLookupResult(std::move(response_->header().headerMap()), + response_->body().length())); + } else { + ENVOY_LOG(debug, "Didn't find unified response for key {}u", variant_hash_key_); + // Unlike DIVIDED mode, lock is not tried to be acquired before insertion. + // Instead, when putting a unified response into cache, putIfAbsent is called + // and hence only one insertion is performed. Cost for the creation of the + // unified entry (simultaneously) by multiple contexts is preferred + // over locking mechanism here. + cb(LookupResult{}); + } +} + +void UnifiedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) { + ENVOY_LOG(debug, "Getting unified body (total length = {}) with range from {} to {}", + response_->body().length(), range.begin(), range.end()); + ASSERT(response_ && !abort_insertion_); + ASSERT(range.end() <= response_->body().length()); + hazelcast::byte* data = response_->body().begin() + range.begin(); + cb(std::make_unique(data, range.length())); +} + +UnifiedInsertContext::UnifiedInsertContext(LookupContext& lookup_context, + HazelcastHttpCache& cache) : HazelcastInsertContextBase(lookup_context, cache) {} + +void UnifiedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response_headers, + bool end_stream) { + if (abort_insertion_) { + return; + } + ASSERT(!committed_end_stream_); + header_map_ = Http::createHeaderMap(response_headers); + if (end_stream) { + flushEntry(); + } +} + +void UnifiedInsertContext::insertBody(const Buffer::Instance& chunk, + InsertCallback ready_for_next_chunk, bool end_stream) { + if (abort_insertion_) { + if (ready_for_next_chunk) { + ready_for_next_chunk(false); + } + return; + } + ASSERT(!committed_end_stream_); + size_t buffer_length = buffer_vector_.size(); + size_t allowed_size = max_body_size_ - buffer_length; + if (allowed_size > chunk.length()) { + buffer_vector_.resize(buffer_length + chunk.length()); + chunk.copyOut(0, chunk.length(), buffer_vector_.data() + buffer_length); + } else { + // Store the body copied until now and abort the further attempted. + buffer_vector_.resize(max_body_size_); + chunk.copyOut(0, allowed_size, buffer_vector_.data() + buffer_length); + flushEntry(); + ready_for_next_chunk(false); + return; + } + + if (end_stream) { + flushEntry(); + } else if (ready_for_next_chunk) { + ready_for_next_chunk(true); + } +} + +void UnifiedInsertContext::flushEntry() { + ASSERT(!abort_insertion_); + ASSERT(!committed_end_stream_); + ENVOY_LOG(debug, "Inserting unified entry if absent with key {}u", variant_hash_key_); + committed_end_stream_ = true; + + // Versions are not necessary for unified entries. Hence passing arbitrary 0 here. + HazelcastHeaderEntry header(std::move(header_map_), std::move(variant_key_), + buffer_vector_.size(), 0); + HazelcastBodyEntry body(variant_hash_key_, std::move(buffer_vector_), 0); + + HazelcastResponseEntry entry(std::move(header), std::move(body)); + try { + hz_cache_.putResponseIfAbsent(variant_hash_key_, entry); + } catch (HazelcastClientOfflineException e) { + ENVOY_LOG(warn, "Hazelcast cluster connection is lost!"); } +} + +DividedLookupContext::DividedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request) + : HazelcastLookupContextBase(cache, std::move(request)), + body_partition_size_(cache.bodySizePerEntry()) {}; + +void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { + ENVOY_LOG(debug, "Looking up divided header with key {}u", variant_hash_key_); + HazelcastHeaderPtr header_entry; + try { + header_entry = hz_cache_.getHeader(variant_hash_key_); + } catch (HazelcastClientOfflineException e){ + ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" + "until the connection is restored..."); + abort_insertion_ = true; + cb(LookupResult{}); + return; + } + if (header_entry) { + ENVOY_LOG(debug, "Found divided response for key {}u, version {}, body size = {}", + variant_hash_key_, header_entry->version(), header_entry->bodySize()); + if (!MessageDifferencer::Equals(header_entry->variantKey(), variantKey())) { + // The same logic with UnifiedLookupContext#getHeaders applies. + ENVOY_LOG(debug, "Keys mismatched for hash {}u. " + "Aborting lookup & insertion", variant_hash_key_); + abort_insertion_ = true; + cb(LookupResult{}); + return; + } + this->total_body_size_ = header_entry->bodySize(); + this->version_ = header_entry->version(); + cb(lookup_request_.makeLookupResult(std::move(header_entry->headerMap()), total_body_size_)); + } else { + ENVOY_LOG(debug, "Didn't find divided response for key {}u", variant_hash_key_); + // To prevent multiple insertion contexts to create the same response in the cache, + // mark only one of them responsible for the insertion using Hazelcast map key locks. + // If key is not locked, it will be acquired here and only one insertion context + // created for this lookup will be responsible for the insertion. This is also valid + // when multiple cache filters from different proxies are connected to the same + // Hazelcast cluster. + try { + abort_insertion_ = !hz_cache_.tryLock(variant_hash_key_); + } catch (HazelcastClientOfflineException e) { + ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" + " until the connection is restored..."); + abort_insertion_ = true; + } + cb(LookupResult{}); + } +} + +// Hence bodies are stored partially on the cache (see hazelcast_cache_entry.h for details), +// the returning buffer from this function can have a size of at most body_partition_size_. +// The caller (filter) has to check range and make another getBody request if needed. +// +// For instance, for a response of which body is 5 MB length, the cached entries will look +// like the following with 2 MB of body_partition_size_ configured: +// +// --> HazelcastHeaderEntry(response headers) +// +// --> HazelcastBodyEntry(0-2 MB) +// --> HazelcastBodyEntry(2-4 MB) +// --> HazelcastBodyEntry(4-5 MB) +// +void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) { + ASSERT(range.end() <= total_body_size_); + ASSERT(!abort_insertion_); + + // Lookup for only one body partition which includes the range.begin(). + uint64_t body_index = range.begin() / body_partition_size_; + HazelcastBodyPtr body; + try { + body = hz_cache_.getBody(variant_hash_key_, body_index); + } catch (HazelcastClientOfflineException e) { + ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" + " until the connection is restored..."); + cb(nullptr); + return; + } + + if (body) { + ENVOY_LOG(debug, "Found divided body with key {}u + \"{}\", version {}, size {}", + variant_hash_key_, body_index, body->version(),body->length()); + if (body->version() != version_) { + ENVOY_LOG(debug, "Body version mismatched with header for key {}u at body: {}. " + "Aborting lookup and performing cleanup.", variant_hash_key_, body_index); + hz_cache_.onVersionMismatch(variant_hash_key_, version_, total_body_size_); + cb(nullptr); + return; + } + uint64_t offset = (range.begin() % body_partition_size_); + hazelcast::byte* data = body->begin() + offset; + if (range.end() < (body_index + 1) * body_partition_size_) { + // No other body partition is needed since this one satisfies the + // range. Callback with the appropriate body bytes. + cb(std::make_unique(data, range.length())); + } else { + // The range requests bytes from the next body partition as well. + // Callback with the bytes until the end of the current partition. + cb(std::make_unique(data, body->length() - offset)); + } + } else { + // Body partition is expected to reside in the cache but lookup is failed. + ENVOY_LOG(debug, "Found missing body for key {}u at body: {}. Cleaning up response" + "with body size: {}", variant_hash_key_, body_index, total_body_size_); + hz_cache_.onMissingBody(variant_hash_key_, version_, total_body_size_); + cb(nullptr); + } +}; + +DividedInsertContext::DividedInsertContext(LookupContext& lookup_context, + HazelcastHttpCache& cache) : HazelcastInsertContextBase(lookup_context, cache), + body_partition_size_(cache.bodySizePerEntry()), version_(createVersion()) {} + +void DividedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response_headers, + bool end_stream) { + if (abort_insertion_) { + return; + } + ASSERT(!committed_end_stream_); + header_map_ = Http::createHeaderMap(response_headers); + if (end_stream) { + flushHeader(); + } +} + +// Body insertions in DIVIDED cache mode must be performed over a fixed sized buffer +// hence continuity of the body partitions are ensured. To do this, insertion chunk's +// content is copied into a local buffer every time insertBody is called. And it is +// flushed when it reaches the maximum capacity (body_partition_size_). +void DividedInsertContext::insertBody(const Buffer::Instance& chunk, + InsertCallback ready_for_next_chunk, bool end_stream) { + if (abort_insertion_) { + ENVOY_LOG(debug, "Aborting insertion for the hash key: {}", variant_hash_key_); + if (ready_for_next_chunk) { + ready_for_next_chunk(false); + } + return; + } + ASSERT(!committed_end_stream_); + uint64_t copied_bytes = 0; + uint64_t allowed_bytes = max_body_size_ - + (body_order_ * body_partition_size_ + buffer_vector_.size()); + uint64_t remaining_bytes = allowed_bytes < chunk.length() ? allowed_bytes : chunk.length(); + bool trimmed = remaining_bytes == allowed_bytes; + while (remaining_bytes) { + uint64_t available_bytes = body_partition_size_ - buffer_vector_.size(); + if (available_bytes < remaining_bytes) { + // This chunk is going to fill the buffer. Copy as much bytes as possible + // into the buffer, flush the buffer and continue with the remaining bytes. + copyIntoLocalBuffer(copied_bytes, available_bytes, chunk); + ASSERT(buffer_vector_.size() == body_partition_size_); + remaining_bytes -= available_bytes; + flushBuffer(); + } else { + // Copy all the bytes starting from chunk[copied_bytes] into buffer. Current + // buffer can hold the remaining data. + copyIntoLocalBuffer(copied_bytes, remaining_bytes, chunk); + break; + } + } + + if (end_stream || trimmed) { + // Header shouldn't be inserted before body insertions are completed. + // Total body size in the header entry is computed via inserted body partitions. + flushBuffer(); + flushHeader(); + } + if (ready_for_next_chunk) { + ready_for_next_chunk(!trimmed); + } +} + +void DividedInsertContext::copyIntoLocalBuffer(uint64_t& offset, uint64_t& size, + const Buffer::Instance& source) { + uint64_t current_size = buffer_vector_.size(); + buffer_vector_.resize(current_size + size); + source.copyOut(offset, size, buffer_vector_.data() + current_size); + offset += size; +}; + +void DividedInsertContext::flushBuffer() { + ASSERT(!abort_insertion_); + if (buffer_vector_.size() == 0) { + return; + } + total_body_size_ += buffer_vector_.size(); + HazelcastBodyEntry bodyEntry(hz_cache_.mapKey(variant_hash_key_), + std::move(buffer_vector_), version_); + buffer_vector_.clear(); + try { + hz_cache_.putBody(variant_hash_key_, body_order_++, bodyEntry); + } catch (HazelcastClientOfflineException e) { + ENVOY_LOG(warn, "Hazelcast cluster connection is lost!"); + } + if (body_order_ == ConfigUtil::partitionWarnLimit()) { + ENVOY_LOG(warn, "Number of body partitions for a response has been reached {} (or more).", + ConfigUtil::partitionWarnLimit()); + ENVOY_LOG(info, "Having so many partitions might cause performance drop " + "as well as extra memory usage. Consider increasing body" + "partition size."); + } +} + +void DividedInsertContext::flushHeader() { + ASSERT(!abort_insertion_); + ASSERT(!committed_end_stream_); + committed_end_stream_ = true; + HazelcastHeaderEntry header(std::move(header_map_), std::move(variant_key_), + total_body_size_, version_); + try { + hz_cache_.putHeader(variant_hash_key_, header); + hz_cache_.unlock(variant_hash_key_); + ENVOY_LOG(debug, "Inserted header entry with key {}u", variant_hash_key_); + } catch (HazelcastClientOfflineException e) { + ENVOY_LOG(warn, "Hazelcast Connection is offline!"); + // To handle leftover locks, hazelcast.lock.max.lease.time.seconds property must + // be set to a reasonable value on the server side. It is Long.MAX by default. + // To make this independent from the server configuration, tryLock with leaseTime + // option can be used when available in a future release of cpp client. The related + // issue can be tracked at: https://github.com/hazelcast/hazelcast-cpp-client/issues/579 + // TODO(enozcan): Use tryLock with leaseTime when released for Hazelcast cpp client. + } +} + +} // HazelcastHttpCache +} // Cache +} // HttpFilters +} // Extensions +} // Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h new file mode 100644 index 0000000000000..b6b67332964b2 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h @@ -0,0 +1,207 @@ +#pragma once + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +/** + * Base lookup context for both UNIFIED and DIVIDED cache lookups. + */ +class HazelcastLookupContextBase : public LookupContext, + public Logger::Loggable { +public: + HazelcastLookupContextBase(HazelcastHttpCache& cache, LookupRequest&& request) + : hz_cache_(cache), lookup_request_(std::move(request)) { + createVariantKey(lookup_request_.key()); + variant_hash_key_ = stableHashKey(lookup_request_.key()); + } + + void getTrailers(LookupTrailersCallback&&) override { + // TODO(enozcan): Support trailers when implemented on the filter side. + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + + const LookupRequest& request() const { return lookup_request_; } + + const Key& variantKey() const { return lookup_request_.key(); } + + uint64_t variantHashKey() const { return variant_hash_key_; } + + bool isAborted() const { return abort_insertion_; } + +protected: + + HazelcastHttpCache& hz_cache_; + LookupRequest lookup_request_; + + /** Hash key aware of vary headers. Lookup will be performed using this. */ + uint64_t variant_hash_key_; + + /** Flag to notice insert context created with this lookup. */ + bool abort_insertion_ = false; + +private: + + /** + * The keys created by the cache filter for lookups and inserts are not aware + * of the vary headers of the request. Instead, cache filter expects a + * cache to differentiate responses having the same key by their vary + * headers. Rather than storing multiple responses with the same key and + * then querying them according to vary headers, a different key for each + * response including vary headers in custom fields is created here. Hence + * responses can be found by their directly. + * + * @param raw_key Key created by the filter. + */ + // TODO(enozcan): Ensure uniqueness of hash key for the same same response. + // Different hash keys will be created if the order of values differ for the same + // vary header key. Hence the response will not be affected but the same response + // will be cached with different keys. (might be reordering/sorting here). + // Prevent str copies as possible. + void createVariantKey(Key& raw_key) { + ASSERT(raw_key.custom_fields_size() == 0); + ASSERT(raw_key.custom_ints_size() == 0); // Key must be pure. + if (lookup_request_.vary_headers().size() == 0) { + return; + } + std::vector> header_strings; + + for (const Http::HeaderEntry& header : lookup_request_.vary_headers()) { + header_strings.push_back(std::make_pair(std::string(header.key().getStringView()), + std::string(header.value().getStringView()))); + } + std::sort(header_strings.begin(), header_strings.end(), [](auto& left, auto& right) -> bool { + return left.first == right.first ? left.second < right.second : left.first < right.first; + }); + for (auto& header : header_strings) { + raw_key.add_custom_fields(header.first + ":" + header.second); + } + } + +}; + +/** + * Base insert context for both UNIFIED and DIVIDED cache insertions. + */ +class HazelcastInsertContextBase : public InsertContext, + public Logger::Loggable { +public: + HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastHttpCache& cache) + : hz_cache_(cache), max_body_size_(cache.maxBodySize()), + variant_hash_key_(static_cast(lookup_context).variantHashKey()), + variant_key_(static_cast(lookup_context).variantKey()), + abort_insertion_(static_cast(lookup_context).isAborted()) {} + + void insertTrailers(const Http::ResponseTrailerMap&) override { + // TODO(enozcan): Support trailers + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + +protected: + HazelcastHttpCache& hz_cache_; + const uint64_t max_body_size_; + bool committed_end_stream_ = false; + + // From lookup context + const uint64_t variant_hash_key_; + Key variant_key_; + const bool abort_insertion_; + + // Response fields + /** Body content is first copied into this buffer and then written to distributed map. */ + std::vector buffer_vector_; + + Http::ResponseHeaderMapPtr header_map_; +}; + +/** + * Lookup context for UNIFIED cache. + */ +class UnifiedLookupContext : public HazelcastLookupContextBase { +public: + UnifiedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request); + void getHeaders(LookupHeadersCallback&& cb) override; + void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; +private: + HazelcastResponsePtr response_; +}; + +/** + * Insert context for UNIFIED cache. + */ +class UnifiedInsertContext : public HazelcastInsertContextBase { +public: + UnifiedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache); + void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) override; + void insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, + bool end_stream) override; +private: + void flushEntry(); +}; + +/** + * Lookup context for DIVIDED cache. + */ +class DividedLookupContext : public HazelcastLookupContextBase { +public: + DividedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request); + void getHeaders(LookupHeadersCallback&& cb) override; + void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; + +private: + uint64_t total_body_size_; + + int32_t version_; + + /** Max body size per body entry defined via config. */ + const uint64_t body_partition_size_; +}; + +/** + * Insert context for DIVIDED cache. + */ +class DividedInsertContext : public HazelcastInsertContextBase { +public: + DividedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache); + void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) override; + void insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, + bool end_stream) override; + +private: + void copyIntoLocalBuffer(uint64_t& index, uint64_t& size, const Buffer::Instance& source); + void flushBuffer(); + void flushHeader(); + + /** Creates a common version for a header and its body entries. + * This version entity secures the relation between a header and + * its bodies hence they are stored in different distributed maps + * in DIVIDED mode. */ + int32_t createVersion(){ + // TODO: Use a secure or Envoy style random. + // The was designed to be the timestamp fetched from the Hazelcast + // cluster during insertion (clusterTime). However, it's currently not + // supported by the cpp client. Might be available in 4.0 release. + // Related issue is: https://github.com/hazelcast/hazelcast-cpp-client/issues/581 + return std::rand(); + } + + /** Counter for the order of next body entry to be inserted. */ + int body_order_ = 0; + + /** Max body size per body entry defined via config. */ + const uint64_t body_partition_size_; + + const int32_t version_; + uint64_t total_body_size_ = 0; + +}; + +} // HazelcastHttpCache +} // Cache +} // HttpFilters +} // Extensions +} // Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc new file mode 100644 index 0000000000000..e11cbfa149ab5 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -0,0 +1,235 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/config_util.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" + +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +LookupContextPtr HazelcastHttpCache::makeLookupContext(LookupRequest&& request) { + if (unified_) { + return std::make_unique(*this, std::move(request)); + } else { + return std::make_unique(*this, std::move(request)); + } +} + +InsertContextPtr HazelcastHttpCache::makeInsertContext(LookupContextPtr&& lookup_context) { + ASSERT(lookup_context != nullptr); + if (unified_) { + return std::make_unique(*lookup_context, *this); + } else { + return std::make_unique(*lookup_context, *this); + } +} + +void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + // TODO(enozcan): Enable when implemented on the filter side. + ASSERT(lookup_context); + ASSERT(response_headers); + if (unified_) { + updateUnifiedHeaders(std::move(lookup_context), std::move(response_headers)); + } else { + updateDividedHeaders(std::move(lookup_context), std::move(response_headers)); + } +} + +constexpr absl::string_view HazelcastCacheName = "envoy.extensions.http.cache.hazelcast"; + +// Cluster wide cache statistics should be observed on Hazelcast Management Center. +// Hence they are not stored locally. +CacheInfo HazelcastHttpCache::cacheInfo() const { + CacheInfo cache_info; + cache_info.name_ = HazelcastCacheName; + cache_info.supports_range_requests_ = true; + return cache_info; +} + +void HazelcastHttpCache::putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) { + getHeaderMap().set(mapKey(key), entry); +} + +void HazelcastHttpCache::putBody(const uint64_t& key, const uint64_t& order, + const HazelcastBodyEntry& entry) { + getBodyMap().set(orderedMapKey(key, order), entry); +} + +HazelcastHeaderPtr HazelcastHttpCache::getHeader(const uint64_t& key) { + return getHeaderMap().get(mapKey(key)); +} + +HazelcastBodyPtr HazelcastHttpCache::getBody(const uint64_t& key, const uint64_t& order) { + return getBodyMap().get(orderedMapKey(key, order)); +} + +void HazelcastHttpCache::putResponseIfAbsent(const uint64_t& key, + const HazelcastResponseEntry& entry) { + getResponseMap().putIfAbsent(mapKey(key), entry); +} + +HazelcastResponsePtr HazelcastHttpCache::getResponse(const uint64_t& key) { + return getResponseMap().get(mapKey(key)); +} + +void HazelcastHttpCache::connect() { + if (hazelcast_client_) { + ENVOY_LOG(warn, "Client is already connected. Cluster name: {}", + hazelcast_client_->getClientConfig().getGroupConfig().getName()); + return; + } + + ClientConfig config = ConfigUtil::getClientConfig(cache_config_); + config.getSerializationConfig().addDataSerializableFactory( + HazelcastCacheEntrySerializableFactory::FACTORY_ID, + boost::shared_ptr + (new HazelcastCacheEntrySerializableFactory())); + + try { + hazelcast_client_ = std::make_unique(config); + } catch (...) { + throw EnvoyException("Hazelcast Client could not connect to any cluster."); + } + + ENVOY_LOG(info, "HazelcastHttpCache has been started with profile: {}. Max body size: {}.", + unified_ ? "UNIFIED" : "DIVIDED, partition size: " + std::to_string(body_partition_size_), + max_body_size_); + ENVOY_LOG(info, "Cache statistics can be observed on Hazelcast Management Center" + " from the map named {}.", unified_ ? response_map_name_ : header_map_name_); +} + +void HazelcastHttpCache::shutdown() { + if (hazelcast_client_) { + ENVOY_LOG(info, "Shutting down Hazelcast connection..."); + hazelcast_client_->shutdown(); + hazelcast_client_.release(); + ENVOY_LOG(info, "Cache is offline now."); + } +} + +void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { + try { + if (!tryLock(key)) { + // Let lock owner context to recover it. + return; + } + auto header = getHeader(key); + if (header && header->version() != version) { + // The missed body does not belong to the looked up header. Probably eviction and then + // insertion for the header has happened in the meantime. Since new insertion will + // override the existing bodies, ignore the cleanup and let orphan bodies (belong to + // evicted header, not overridden) be evicted by TTL as well. + unlock(key); + return; + } + int body_count = body_size / body_partition_size_; + while (body_count >= 0) { + getBodyMap().removeAsync(orderedMapKey(key, body_count--)); + } + getHeaderMap().remove(mapKey(key)); + unlock(key); + } catch (HazelcastClientOfflineException e) { + // see DividedInsertContext#flushHeader() for left over locks on a connection failure. + ENVOY_LOG(warn, "Hazelcast Connection is offline!"); + } +}; + +void HazelcastHttpCache::onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) { + onMissingBody(key, version, body_size); +} + +bool HazelcastHttpCache::tryLock(const uint64_t& key) { + return unified_ ? getResponseMap().tryLock(mapKey(key)) : + getHeaderMap().tryLock(mapKey(key)); +} + +void HazelcastHttpCache::unlock(const uint64_t& key) { + // Hazelcast does not allow a thread to unlock a key unless it's the key + // owner. To handle this, forceUnlock is called. + if (unified_) { + getResponseMap().forceUnlock(mapKey(key)); + } else { + getHeaderMap().forceUnlock(mapKey(key)); + } +} + +void HazelcastHttpCache::updateUnifiedHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) { + const uint64_t& key = static_cast(lookup_context.get())->variantHashKey(); + HazelcastResponsePtr response = getResponse(key); + if (!response) { + // Might be evicted in meantime + ENVOY_LOG(debug, "Updating unified headers ({}) is aborted due to lookup miss.", key); + return; + } + HazelcastResponseEntry updated = *response; + updated.header().headerMap(std::move(response_headers)); + // Update headers if no other update is performed in meantime. + getResponseMap().replace(key, updated, *response); +}; + +void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) { + const uint64_t& key = static_cast(lookup_context.get())->variantHashKey(); + HazelcastHeaderPtr stale = getHeader(key); + if (!stale) { + // Might be evicted in meantime + ENVOY_LOG(debug, "Updating divided headers ({}) is aborted due to lookup miss.", key); + return; + } + HazelcastHeaderEntry updated = *stale; + updated.headerMap(std::move(response_headers)); + // Update headers if no other update is performed in meantime. + getHeaderMap().replace(key, updated, *stale); +}; + +std::string HazelcastHttpCache::constructMapName(const std::string& postfix) { + return std::string(cache_config_.app_prefix()) + .append(":") + .append(std::to_string(body_partition_size_)) + .append(":") + .append(postfix); +} + +HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) + : cache_config_(config), unified_(config.unified()), + body_partition_size_(ConfigUtil::validPartitionSize(config.body_partition_size())), + max_body_size_(ConfigUtil::validMaxBodySize(config.max_body_size())) { + body_map_name_ = constructMapName("body"); + header_map_name_ = constructMapName("header"); + response_map_name_ = constructMapName("response"); +}; + +class HazelcastHttpCacheFactory : public HttpCacheFactory { +public: + // From UntypedFactory + std::string name() const override { return std::string(HazelcastCacheName); } + // From TypedFactory + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + // From HttpCacheFactory + HttpCache& + getCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) override { + HazelcastHttpCacheConfig hz_cache_config; + MessageUtil::unpackTo(config.typed_config(), hz_cache_config); + cache_ = std::make_unique(hz_cache_config); + cache_->connect(); + return *cache_; + } +private: + std::unique_ptr cache_; +}; + +static Registry::RegisterFactory register_; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h new file mode 100644 index 0000000000000..27e808c6c502f --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -0,0 +1,129 @@ +#pragma once + +#include "common/common/logger.h" + +#include "extensions/filters/http/cache/http_cache.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" + +#include "hazelcast/client/HazelcastClient.h" +#include "hazelcast/client/IMap.h" + +#include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +using hazelcast::client::IMap; +using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; + +// TODO: Consider putting responses into cache with TTL derived from `age` header. +// instead of using a common TTL for all. + +// TODO: Mention about max.lease.time option on readme doc. + +class HazelcastHttpCache : public HttpCache, + public Logger::Loggable { +public: + HazelcastHttpCache(HazelcastHttpCacheConfig config); + + // Cache::HttpCache + LookupContextPtr makeLookupContext(LookupRequest&& request) override; + InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; + void updateHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) override; + CacheInfo cacheInfo() const override; + + // Divided profile + void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry); + void putBody(const uint64_t& key, const uint64_t& order, const HazelcastBodyEntry& entry); + HazelcastHeaderPtr getHeader(const uint64_t& key); + HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order); + + // Unified profile + void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry); + HazelcastResponsePtr getResponse(const uint64_t& key); + + // Hazelcast cluster connection + void connect(); + void shutdown(); + + // Recoveries for malformed entries + void onMissingBody(uint64_t key, int32_t version, uint64_t body_size); + void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size); + + // Internal lock mechanism of Hazelcast specific to map and key pair is + // used to make exactly one lookup context responsible for insertions and + // secure consistency during updateHeaders(). These locks prevent possible + // race for multiple cache filters from multiple proxies when they connect + // to the same Hazelcast cluster. + bool tryLock(const uint64_t& key); + void unlock(const uint64_t& key); + + const uint64_t& bodySizePerEntry() { return body_partition_size_; }; + const uint64_t& maxBodySize() { return max_body_size_; }; + + inline int64_t mapKey(const uint64_t& unsigned_key) { + // Hazelcast client accepts signed keys on maps. The reason for not static casting + // directly is a possible overflow for int64 on intermediate step for -2^63. + int64_t signed_key; + std::memcpy (&signed_key, &unsigned_key, sizeof(int64_t)); + return signed_key; + } + + inline std::string orderedMapKey(const uint64_t& key, const uint64_t& order) { + return std::to_string(mapKey(key)) + std::to_string(order); + } + + ~HazelcastHttpCache() { + shutdown(); + }; + +private: + friend class HazelcastHttpCacheTestBase; + + void updateUnifiedHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers); + void updateDividedHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers); + + // Maps are differentiated by their names in Hazelcast cluster. Hence each + // plugin will connect to a map named with partition size and app_prefix. + // When a cache connects to a cluster which already has an active cache + // with different body_partition_size, this naming will prevent incompatibility + // and separate these two caches in the Hazelcast cluster. + std::string constructMapName(const std::string& postfix); + + inline IMap getHeaderMap() { + return hazelcast_client_->getMap(header_map_name_); + } + + inline IMap getBodyMap() { + return hazelcast_client_->getMap(body_map_name_); + } + + inline IMap getResponseMap() { + return hazelcast_client_->getMap(response_map_name_); + } + + std::unique_ptr hazelcast_client_; + HazelcastHttpCacheConfig cache_config_; + + // From cache configuration + const bool unified_; + const uint64_t body_partition_size_; + const uint64_t max_body_size_; + + std::string body_map_name_; + std::string header_map_name_; + std::string response_map_name_; + +}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD new file mode 100644 index 0000000000000..5805f6770ae99 --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -0,0 +1,41 @@ +licenses(["notice"]) # Apache 2 + +load("//bazel:envoy_build_system.bzl", "envoy_package") +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", + "envoy_extension_cc_test_library", +) + +envoy_package() + +envoy_extension_cc_test( + name = "hazelcast_unified_cache_test", + srcs = ["hazelcast_unified_cache_test.cc"], + extension_name = "envoy.filters.http.cache.hazelcast_http_cache", + deps = [ + ":hazelcast_test_util_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "hazelcast_divided_cache_test", + srcs = ["hazelcast_divided_cache_test.cc"], + extension_name = "envoy.filters.http.cache.hazelcast_http_cache", + deps = [ + ":hazelcast_test_util_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test_library( + name = "hazelcast_test_util_lib", + hdrs = ["util.h"], + extension_name = "envoy.filters.http.cache.hazelcast_http_cache", + deps = [ + "//source/extensions/filters/http/cache/hazelcast_http_cache:hazelcast_http_cache_lib", + ], +) diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc new file mode 100644 index 0000000000000..ac4e6d03be28e --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -0,0 +1,419 @@ +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" + +#include "envoy/registry/registry.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { +protected: + static void SetUpTestSuite() { + HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(false); + hz_cache_ = new HazelcastHttpCache(cfg); + hz_cache_->connect(); + clearMaps(); + } +}; + +TEST_F(HazelcastDividedCacheTest, SimpleMissAndPutEntries) { + const std::string RequestPath1("/size/minus/one"); + const std::string RequestPath2("/exactly/size"); + const std::string RequestPath3("/size/plus/one"); + + LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr name_lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr name_lookup_context3 = lookup(RequestPath3); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body1("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 - 1); + const std::string Body2("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2); + const std::string Body3("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 + 1); + + insert(move(name_lookup_context1), getResponseHeaders(), Body1); + insert(move(name_lookup_context2), getResponseHeaders(), Body2); + insert(move(name_lookup_context3), getResponseHeaders(), Body3); + + name_lookup_context1 = lookup(RequestPath1); + name_lookup_context2 = lookup(RequestPath2); + name_lookup_context3 = lookup(RequestPath3); + + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context2.get(), Body2)); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context3.get(), Body3)); + + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, HandleRangedResponses) { + // make the size even for the sake of divisions below. + const int size = HazelcastTestUtil::TEST_PARTITION_SIZE & 1 ? + HazelcastTestUtil::TEST_PARTITION_SIZE + 1 : + HazelcastTestUtil::TEST_PARTITION_SIZE; + + const std::string RequestPath("/ranged/responses"); + + LookupContextPtr name_lookup_context1 = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body = std::string(size, 'h') + + std::string(size, 'z') + std::string(size, 'c'); + + insert(move(name_lookup_context1), getResponseHeaders(), Body); + name_lookup_context1 = lookup(RequestPath); + + EXPECT_EQ(std::string(size, 'h'), getBody(*name_lookup_context1, 0, size)); + EXPECT_EQ(std::string(size, 'z'), getBody(*name_lookup_context1, size, size * 2)); + EXPECT_EQ(std::string(size / 2, 'h') + std::string(size / 2, 'z'), + getBody(*name_lookup_context1, size / 2, size + size / 2)); + EXPECT_EQ(std::string("h") + std::string(size, 'z') + std::string("c"), + getBody(*name_lookup_context1, size - 1, 2 * size + 1)); + EXPECT_EQ(Body, getBody(*name_lookup_context1, 0, size * 3)); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { + const std::string RequestPath("/abort/when/max/size/reached"); + InsertContextPtr inserter = hz_cache_->makeInsertContext(lookup(RequestPath)); + inserter->insertHeaders(getResponseHeaders(), false); + bool ready_for_next = true; + while (ready_for_next) { + inserter->insertBody( + Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), + [&](bool ready){ready_for_next = ready;}, false); + } + + EXPECT_EQ( + (HazelcastTestUtil::TEST_MAX_BODY_SIZE / HazelcastTestUtil::TEST_PARTITION_SIZE) + + ((HazelcastTestUtil::TEST_MAX_BODY_SIZE % HazelcastTestUtil::TEST_PARTITION_SIZE) == 0 ? 0 : 1), + testBodyMap().size()); + + LookupContextPtr name_lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody( + name_lookup_context.get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { + const std::string RequestPath("/only/one/must/insert"); + + LookupContextPtr name_lookup_context1 = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + // The first missed lookup must be allowed to make insertion. + ASSERT(!static_cast(*name_lookup_context1).isAborted()); + + // Following ones must abort the insertion. + LookupContextPtr name_lookup_context2; + std::thread t1([&] { + // If the second lookup would not be performed in a separate thread, it will acquire + // the lock even if it's already locked. This is because the key locks on Hazelcast + // IMap are re-entrant. A locked key can be acquired by the same thread again and + // again based on its pid. + // TODO: Examine Envoy's threading model to ensure this case's safety. + name_lookup_context2 = lookup(RequestPath); + }); + t1.join(); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + ASSERT(static_cast(*name_lookup_context2).isAborted()); + + const std::string Body("hazelcast"); + // second context should not insert even if arrives before the first one. + insert(move(name_lookup_context2), getResponseHeaders(), Body); + name_lookup_context2 = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // first one must do the insertion. + insert(move(name_lookup_context1), getResponseHeaders(), Body); + name_lookup_context1 = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body)); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { + const std::string RequestPath1("/miss/on/version/mismatch"); + + LookupContextPtr name_lookup_context = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + uint64_t variant_hash_key = + static_cast(*name_lookup_context).variantHashKey(); + + const std::string Body(HazelcastTestUtil::TEST_PARTITION_SIZE * 2, 'h'); + insert(move(name_lookup_context), getResponseHeaders(), Body); + name_lookup_context = lookup(RequestPath1); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), Body)); + + // Change version of the second partition. + const std::string body2key = getBodyKey(variant_hash_key,1); + auto body2 = testBodyMap().get(body2key); + EXPECT_NE(body2, nullptr); + body2->version(body2->version() + 1); + testBodyMap().put(body2key, *body2); + + // Change happened in the second partition. Lookup to the first one should be successful. + name_lookup_context = lookup(RequestPath1); + std::string partition1 = getBody(*name_lookup_context, 0, HazelcastTestUtil::TEST_PARTITION_SIZE); + EXPECT_EQ(partition1, std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')); + + std::string fullBody = getBody(*name_lookup_context, 0, HazelcastTestUtil::TEST_PARTITION_SIZE * 2); + EXPECT_EQ(fullBody, HazelcastTestUtil::abortedBodyResponse()); + + // Clean up must be performed for malformed entries. + EXPECT_EQ(0, testBodyMap().size()); + EXPECT_EQ(0, testHeaderMap().size()); + + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, MissLookupOnDifferentKey) { + + const std::string RequestPath("/miss/on/different/key"); + + LookupContextPtr name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + uint64_t variant_hash_key = + static_cast(*name_lookup_context).variantHashKey(); + + const std::string Body("hazelcast"); + insert(move(name_lookup_context), getResponseHeaders(), Body); + name_lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), Body)); + + // Manipulate the cache entry directly. Cache is not aware of that. + // The cached key will not be the same with the created one by filter. + auto header = testHeaderMap().get(mapKey(variant_hash_key)); + Key modified = header->variantKey(); + modified.add_custom_fields("custom1"); + modified.add_custom_fields("custom2"); + header->variantKey(std::move(modified)); + testHeaderMap().put(mapKey(variant_hash_key), *header); + + name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // New entry insertion should be aborted and not override the existing one with the + // same hash key. This scenario is possible if there is a hash collision. No eviction + // or clean up is expected. Since overriding an entry is prevented. + insert(move(name_lookup_context), getResponseHeaders(), Body); + name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + EXPECT_EQ(1, testHeaderMap().size()); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { + const std::string RequestPath1("/clean/up/on/missing/body"); + LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + uint64_t variant_hash_key = + static_cast(*name_lookup_context1).variantHashKey(); + + const std::string Body = std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h') + + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z') + + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'); + + insert(move(name_lookup_context1), getResponseHeaders(), Body); + name_lookup_context1 = lookup(RequestPath1); + + // Response is cached with the following pattern: + // variant_hash_key -> HeaderEntry (in header map) + // variant_hash_key "0" -> Body1 (in body map) + // variant_hash_key "1" -> Body2 (in body map) + // variant_hash_key "2" -> Body3 (in body map) + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body)); + + removeBody(variant_hash_key, 1); // evict Body2. + + name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + + // Lookup for Body1 is OK. + name_lookup_context1->getBody({0, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, + [](Buffer::InstancePtr&& data) {EXPECT_NE(data, nullptr);}); + + // Lookup for Body2 must fail and trigger clean up. + name_lookup_context1->getBody( + {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, + [](Buffer::InstancePtr&& data){EXPECT_EQ(data, nullptr);}); + + name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // On lookup miss, lock is being acquired. It must be released + // explicitly or let context do the insertion and then release. + // If not released, the second run for the test fails. Since no + // insertion follows the missed lookup here, the lock is explicitly + // unlocked. + hz_cache_->unlock(variant_hash_key); + + // Assert clean up + EXPECT_EQ(0, testBodyMap().size()); + EXPECT_EQ(0, testHeaderMap().size()); +} + +TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { + const std::string RequestPath("/header/only/response"); + LookupContextPtr name_lookup_context = lookup(RequestPath); + uint64_t variant_hash_key = + static_cast(*name_lookup_context).variantHashKey(); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + insert(move(name_lookup_context), getResponseHeaders(), ""); + name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + auto header = testHeaderMap().get(mapKey(variant_hash_key)); + EXPECT_NE(nullptr, header); + EXPECT_EQ(0, header->bodySize()); + EXPECT_EQ(0, testBodyMap().size()); + clearMaps(); +} + +// Tests belong to SimpleHttpCache are applied below with minor changes on the test body. +TEST_F(HazelcastDividedCacheTest, SimplePutGet) { + const std::string RequestPath1("/simple/put/first"); + LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body1("hazelcast"); + insert(move(name_lookup_context1), getResponseHeaders(), Body1); + + name_lookup_context1 = lookup(RequestPath1); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); + + const std::string& RequestPath2("/simple/put/second"); + LookupContextPtr name_lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body2("hazelcast.distributed.caching"); + + insert(move(name_lookup_context2), getResponseHeaders(), Body2); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, PrivateResponse) { + const std::string request_path("/private/response"); + + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body("Value"); + // We must make sure at cache insertion time, private responses must not be + // inserted. However, if the insertion did happen, it would be served at the + // time of lookup. + insert(move(name_lookup_context), getResponseHeaders(), Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, Miss) { + LookupContextPtr name_lookup_context = lookup("/no/such/entry"); + uint64_t variant_hash_key = + static_cast(*name_lookup_context).variantHashKey(); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // Do not left over a missed lookup without inserting or releasing its lock. + hz_cache_->unlock(variant_hash_key); +} + +TEST_F(HazelcastDividedCacheTest, Fresh) { + const Http::TestResponseHeaderMapImpl response_headers = { + {"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public, max-age=3600"}}; + insert("/", response_headers, ""); + time_source_.sleep(std::chrono::seconds(3600)); + lookup("/"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, Stale) { + const Http::TestResponseHeaderMapImpl response_headers = { + {"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public, max-age=3600"}}; + insert("/", response_headers, ""); + time_source_.sleep(std::chrono::seconds(3601)); + lookup("/"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, RequestSmallMinFresh) { + request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "min-fresh=1000"); + const std::string request_path("/request/small/min/fresh"); + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + Http::TestResponseHeaderMapImpl response_headers{ + {"date", formatter_.fromTime(current_time_)}, + {"age", "6000"}, + {"cache-control", "public, max-age=9000"}}; + const std::string Body("Value"); + insert(move(name_lookup_context), response_headers, Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, ResponseStaleWithRequestLargeMaxStale) { + request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "max-stale=9000"); + + const std::string request_path("/response/stale/with/request/large/max/stale"); + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + Http::TestResponseHeaderMapImpl response_headers{ + {"date", formatter_.fromTime(current_time_)}, + {"age", "7200"}, + {"cache-control", "public, max-age=3600"}}; + + const std::string Body("Value"); + insert(move(name_lookup_context), response_headers, Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + clearMaps(); +} + +TEST_F(HazelcastDividedCacheTest, StreamingPut) { + InsertContextPtr inserter = hz_cache_-> + makeInsertContext(lookup("/streaming/put")); + inserter->insertHeaders(getResponseHeaders(), false); + inserter->insertBody( + Buffer::OwnedImpl("Hello, "), + [](bool ready){EXPECT_TRUE(ready); }, false); + inserter->insertBody(Buffer::OwnedImpl("World!"), nullptr, true); + LookupContextPtr name_lookup_context = lookup("/streaming/put"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + EXPECT_NE(nullptr, lookup_result_.headers_); + ASSERT_EQ(13, lookup_result_.content_length_); + EXPECT_EQ("Hello, World!", getBody(*name_lookup_context, 0, 13)); + EXPECT_EQ("o, World!", getBody(*name_lookup_context, 4, 13)); + clearMaps(); +} + +TEST(Registration, GetFactory) { + HttpCacheFactory* factory = Registry::FactoryRegistry::getFactoryByType( + "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); + ASSERT_NE(factory, nullptr); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig config; + HazelcastHttpCacheConfig hz_cache_config = HazelcastTestUtil::getTestConfig(false); + config.mutable_typed_config()->PackFrom(hz_cache_config); + HazelcastHttpCache& cache = static_cast(factory->getCache(config)); + EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); + + // Explicitly destroy Hazelcast connection here. Otherwise the test + // environment does not wait for cache destructor to be completed + // and this causes segfault when Hazelcast Client is shutting down. + cache.shutdown(); +} + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc new file mode 100644 index 0000000000000..65b4e240f0aa0 --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -0,0 +1,321 @@ +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" + +#include "envoy/registry/registry.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { +protected: + static void SetUpTestSuite() { + HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(true); + hz_cache_ = new HazelcastHttpCache(cfg); + hz_cache_->connect(); + clearMaps(); + } +}; + +TEST_F(HazelcastUnifiedCacheTest, SimpleMissAndPutEntries) { + const std::string RequestPath1("/size/minus/one"); + const std::string RequestPath2("/exactly/size"); + const std::string RequestPath3("/size/plus/one"); + + LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr name_lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr name_lookup_context3 = lookup(RequestPath3); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body1("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 - 1); + const std::string Body2("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2); + const std::string Body3("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 + 1); + + insert(move(name_lookup_context1), getResponseHeaders(), Body1); + insert(move(name_lookup_context2), getResponseHeaders(), Body2); + insert(move(name_lookup_context3), getResponseHeaders(), Body3); + + name_lookup_context1 = lookup(RequestPath1); + name_lookup_context2 = lookup(RequestPath2); + name_lookup_context3 = lookup(RequestPath3); + + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context2.get(), Body2)); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context3.get(), Body3)); + + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, HandleRangedResponses) { + // make the size even for the sake of divisions below. + const int size = HazelcastTestUtil::TEST_PARTITION_SIZE & 1 ? + HazelcastTestUtil::TEST_PARTITION_SIZE + 1 : + HazelcastTestUtil::TEST_PARTITION_SIZE; + + const std::string RequestPath("/ranged/responses"); + + LookupContextPtr name_lookup_context1 = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body = std::string(size, 'h') + + std::string(size, 'z') + std::string(size, 'c'); + + insert(move(name_lookup_context1), getResponseHeaders(), Body); + name_lookup_context1 = lookup(RequestPath); + + EXPECT_EQ(std::string(size, 'h'), getBody(*name_lookup_context1, 0, size)); + EXPECT_EQ(std::string(size, 'z'), getBody(*name_lookup_context1, size, size * 2)); + EXPECT_EQ(std::string(size / 2, 'h') + std::string(size / 2, 'z'), + getBody(*name_lookup_context1, size / 2, size + size / 2)); + EXPECT_EQ(std::string("h") + std::string(size, 'z') + std::string("c"), + getBody(*name_lookup_context1, size - 1, 2 * size + 1)); + EXPECT_EQ(Body, getBody(*name_lookup_context1, 0, size * 3)); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedInsertionWhenMaxSizeReached) { + const std::string RequestPath("/abort/when/max/size/reached"); + InsertContextPtr inserter = hz_cache_->makeInsertContext(lookup(RequestPath)); + inserter->insertHeaders(getResponseHeaders(), false); + bool ready_for_next = true; + while (ready_for_next) { + inserter->insertBody( + Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE / 3, 'h')), + [&](bool ready){ready_for_next = ready;}, false); + } + + LookupContextPtr name_lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody( + name_lookup_context.get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, PutResponseIfAbsent) { + const std::string RequestPath("/only/one/must/insert"); + + LookupContextPtr name_lookup_context1 = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + LookupContextPtr name_lookup_context2 = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body1("hazelcast"); + const std::string Body2("hazelcast.distributed.caching"); + + // The second context should insert if the cache is empty for this request. + insert(move(name_lookup_context1), getResponseHeaders(), Body1); + name_lookup_context1 = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); + + // The first context should not do the insertion/override the existing value. + insert(move(name_lookup_context2), getResponseHeaders(), Body2); + // Response body must remain as Body1 + name_lookup_context2 = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context2.get(), Body1)); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, DoNotOverrideExistingResponse) { + const std::string RequestPath1("/on/unified/not/override"); + + LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr name_lookup_context2 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body1("hazelcast-first"); + const std::string Body2("hazelcast-second"); + + insert(move(name_lookup_context1), getResponseHeaders(), Body1); + insert(move(name_lookup_context2), getResponseHeaders(), Body2); + + name_lookup_context1 = lookup(RequestPath1); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, MissLookupOnDifferentKey) { + + const std::string RequestPath("/miss/on/different/key"); + + LookupContextPtr name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + uint64_t variant_hash_key = + static_cast(*name_lookup_context).variantHashKey(); + + const std::string Body("hazelcast"); + insert(move(name_lookup_context), getResponseHeaders(), Body); + name_lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), Body)); + + // Manipulate the cache entry directly. Cache is not aware of that. + // The cached key will not be the same with the created one by filter. + auto response = testResponseMap().get(mapKey(variant_hash_key)); + Key modified = response->header().variantKey(); + modified.add_custom_fields("custom1"); + modified.add_custom_fields("custom2"); + response->header().variantKey(std::move(modified)); + testResponseMap().put(mapKey(variant_hash_key), *response); + + name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // New entry insertion should be aborted and not override the existing one with the + // same hash key. This scenario is possible if there is a hash collision. No eviction + // or clean up is expected. Since overriding an entry is prevented. + insert(move(name_lookup_context), getResponseHeaders(), Body); + name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + EXPECT_EQ(1, testResponseMap().size()); + clearMaps(); +} + +// Tests belong to SimpleHttpCache are applied below with minor changes on the test body. +TEST_F(HazelcastUnifiedCacheTest, SimplePutGet) { + const std::string RequestPath1("/simple/put/first"); + LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body1("hazelcast"); + insert(move(name_lookup_context1), getResponseHeaders(), Body1); + + name_lookup_context1 = lookup(RequestPath1); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); + + const std::string& RequestPath2("/simple/put/second"); + LookupContextPtr name_lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body2("hazelcast.distributed.caching"); + + insert(move(name_lookup_context2), getResponseHeaders(), Body2); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, PrivateResponse) { + const std::string request_path("/private/response"); + + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body("Value"); + // We must make sure at cache insertion time, private responses must not be + // inserted. However, if the insertion did happen, it would be served at the + // time of lookup. + insert(move(name_lookup_context), getResponseHeaders(), Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, Miss) { + LookupContextPtr name_lookup_context = lookup("/no/such/entry"); + uint64_t variant_hash_key = + static_cast(*name_lookup_context).variantHashKey(); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // Do not left over a missed lookup without inserting or releasing its lock. + hz_cache_->unlock(variant_hash_key); +} + +TEST_F(HazelcastUnifiedCacheTest, Fresh) { + const Http::TestResponseHeaderMapImpl response_headers = { + {"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public, max-age=3600"}}; + insert("/", response_headers, ""); + time_source_.sleep(std::chrono::seconds(3600)); + lookup("/"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, Stale) { + const Http::TestResponseHeaderMapImpl response_headers = { + {"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public, max-age=3600"}}; + insert("/", response_headers, ""); + time_source_.sleep(std::chrono::seconds(3601)); + lookup("/"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, RequestSmallMinFresh) { + request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "min-fresh=1000"); + const std::string request_path("/request/small/min/fresh"); + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + Http::TestResponseHeaderMapImpl response_headers{ + {"date", formatter_.fromTime(current_time_)}, + {"age", "6000"}, + {"cache-control", "public, max-age=9000"}}; + const std::string Body("Value"); + insert(move(name_lookup_context), response_headers, Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, ResponseStaleWithRequestLargeMaxStale) { + request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "max-stale=9000"); + + const std::string request_path("/response/stale/with/request/large/max/stale"); + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + Http::TestResponseHeaderMapImpl response_headers{ + {"date", formatter_.fromTime(current_time_)}, + {"age", "7200"}, + {"cache-control", "public, max-age=3600"}}; + + const std::string Body("Value"); + insert(move(name_lookup_context), response_headers, Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + clearMaps(); +} + +TEST_F(HazelcastUnifiedCacheTest, StreamingPut) { + InsertContextPtr inserter = hz_cache_-> + makeInsertContext(lookup("/streaming/put")); + inserter->insertHeaders(getResponseHeaders(), false); + inserter->insertBody( + Buffer::OwnedImpl("Hello, "), + [](bool ready){EXPECT_TRUE(ready); }, false); + inserter->insertBody(Buffer::OwnedImpl("World!"), nullptr, true); + LookupContextPtr name_lookup_context = lookup("/streaming/put"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + EXPECT_NE(nullptr, lookup_result_.headers_); + ASSERT_EQ(13, lookup_result_.content_length_); + EXPECT_EQ("Hello, World!", getBody(*name_lookup_context, 0, 13)); + EXPECT_EQ("o, World!", getBody(*name_lookup_context, 4, 13)); + clearMaps(); +} + +TEST(Registration, GetFactory) { + HttpCacheFactory* factory = Registry::FactoryRegistry::getFactoryByType( + "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); + ASSERT_NE(factory, nullptr); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig config; + HazelcastHttpCacheConfig hz_cache_config = HazelcastTestUtil::getTestConfig(true); + config.mutable_typed_config()->PackFrom(hz_cache_config); + HazelcastHttpCache& cache = static_cast(factory->getCache(config)); + EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); + + // Explicitly destroy Hazelcast connection here. Otherwise the test + // environment does not wait for cache destructor to be completed + // and this causes segfault when Hazelcast Client is shutting down. + cache.shutdown(); +} + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h new file mode 100644 index 0000000000000..adb5ee46be60a --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -0,0 +1,198 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +class HazelcastTestUtil { +public: + + static constexpr int TEST_PARTITION_SIZE = 10; + static constexpr int TEST_MAX_BODY_SIZE = TEST_PARTITION_SIZE * 10; + + static const std::string& abortedBodyResponse() + { + static std::string response("NULL_BODY"); + return response; + } + + static HazelcastHttpCacheConfig getTestConfig(bool unified) { + HazelcastHttpCacheConfig hc; + hc.set_group_name("dev"); + hc.set_group_password("dev-pass"); + hc.set_ip("127.0.0.1"); + hc.set_port(5701); + hc.set_body_partition_size(TEST_PARTITION_SIZE); + hc.set_app_prefix("test"); + hc.set_unified(unified); + hc.set_max_body_size(TEST_MAX_BODY_SIZE); + return hc; + } + + static void setRequestHeaders(Http::TestRequestHeaderMapImpl& headers) { + headers.setMethod("GET"); + headers.setHost("hazelcast.com"); + headers.setForwardedProto("https"); + headers.setCacheControl("max-age=3600"); + } + +}; + +// TODO: If running a Hazelcast server is not possible during tests, +// this base will serve mock cache for testing. +class HazelcastHttpCacheTestBase : public testing::Test { +protected: + + HazelcastHttpCacheTestBase() { + HazelcastTestUtil::setRequestHeaders(request_headers_); + } + + static void TearDownTestSuite() { + delete hz_cache_; + hz_cache_ = nullptr; + } + + // Performs a cache lookup. + LookupContextPtr lookup(absl::string_view request_path) { + LookupRequest request = makeLookupRequest(request_path); + LookupContextPtr context = hz_cache_->makeLookupContext(std::move(request)); + context->getHeaders([this](LookupResult&& result) {lookup_result_ = std::move(result); }); + return context; + } + + // Inserts a value into the cache. + void insert(LookupContextPtr lookup, const Http::TestResponseHeaderMapImpl& response_headers, + const absl::string_view response_body) { + InsertContextPtr inserter = hz_cache_->makeInsertContext(move(lookup)); + inserter->insertHeaders(response_headers, false); + inserter->insertBody(Buffer::OwnedImpl(response_body), nullptr, true); + } + + void insert(absl::string_view request_path, + const Http::TestResponseHeaderMapImpl& response_headers, + const absl::string_view response_body) { + insert(lookup(request_path), response_headers, response_body); + } + + // Makes getBody requests until requested range is satisfied. + // Returns the bod on success, HazelcastTestUtil::abortedBodyResponse() on + // abortion by cache itself. + std::string getBody(LookupContext& context, uint64_t start, uint64_t end) { + std::string full_body, body_chunk; + uint64_t offset = start; + bool aborted = false; + while (full_body.length() != end - start) { + if (aborted) { + return HazelcastTestUtil::abortedBodyResponse(); + } + AdjustedByteRange range(offset, end); + context.getBody(range, [&aborted, &body_chunk, &offset, + &full_body](Buffer::InstancePtr&& data) { + if (data) { + body_chunk = data->toString(); + full_body.append(body_chunk); + offset += body_chunk.length(); + } else { + aborted = true; + } + }); + } + return full_body; + } + + LookupRequest makeLookupRequest(absl::string_view request_path) { + request_headers_.setPath(request_path); + return LookupRequest(request_headers_, current_time_); + } + + AssertionResult expectLookupSuccessWithBody(LookupContext* lookup_context, + absl::string_view body) { + if (lookup_result_.cache_entry_status_ != CacheEntryStatus::Ok) { + return AssertionFailure() << "Expected: lookup_result_.cache_entry_status" + " == CacheEntryStatus::Ok\n Actual: " + << lookup_result_.cache_entry_status_; + } + if (!lookup_result_.headers_) { + return AssertionFailure() << "Expected nonnull lookup_result_.headers"; + } + if (!lookup_context) { + return AssertionFailure() << "Expected nonnull lookup_context"; + } + const std::string actual_body = getBody(*lookup_context, 0, body.size()); + if (body != actual_body) { + return AssertionFailure() << "Expected body == " << body << + "\n Actual: " << actual_body; + } + return AssertionSuccess(); + } + + Http::TestResponseHeaderMapImpl getResponseHeaders() { + return Http::TestResponseHeaderMapImpl{ + {"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public,max-age=3600"}}; + } + + static HazelcastHttpCache* hz_cache_; + LookupResult lookup_result_; + Http::TestRequestHeaderMapImpl request_headers_; + Event::SimulatedTimeSystem time_source_; + SystemTime current_time_ = time_source_.systemTime(); + DateFormatter formatter_{"%a, %d %b %Y %H:%M:%S GMT"}; + + // Helpers for test environment. + static void clearMaps() { + if (hz_cache_->unified_) { + hz_cache_->getResponseMap().clear(); + } else { + hz_cache_->getBodyMap().clear(); + hz_cache_->getHeaderMap().clear(); + } + } + + void removeBody(uint64_t key, uint64_t order) { + ASSERT(!hz_cache_->unified_); + hz_cache_->getBodyMap().remove(hz_cache_->orderedMapKey(key, order)); + } + + IMap testHeaderMap() { + ASSERT(!hz_cache_->unified_); + return hz_cache_->getHeaderMap(); + }; + + IMap testBodyMap() { + ASSERT(!hz_cache_->unified_); + return hz_cache_->getBodyMap(); + }; + + IMap testResponseMap() { + ASSERT(hz_cache_->unified_); + return hz_cache_->getResponseMap(); + }; + + bool isUnified() { + return hz_cache_->unified_; + } + + std::string getBodyKey(uint64_t key, uint64_t order){ + return hz_cache_->orderedMapKey(key, order); + } + + int64_t mapKey(uint64_t key){ + return hz_cache_->mapKey(key); + } +}; + +// Since creating the cache (connecting to the cluster), +// is not light weight, use a single cache through the test environment. +// TODO: Check for parallel tests. Using a random app_prefix per test might solve +// the concurrent request issues. However, destroying cache is still a problem. +HazelcastHttpCache* HazelcastHttpCacheTestBase::hz_cache_ = nullptr; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy From c39db109e6c243963567840179b3e2cf93be0c4b Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Fri, 27 Mar 2020 18:01:27 +0300 Subject: [PATCH 02/33] Secure body key uniqueness and set defaults. Signed-off-by: Enes Ozcan --- .../cache/hazelcast_http_cache/config.proto | 29 +++++++++------ .../cache/hazelcast_http_cache/config_util.h | 36 ++++++++++++------- .../hazelcast_http_cache/hazelcast_context.cc | 2 +- .../hazelcast_http_cache.cc | 16 ++++----- .../hazelcast_http_cache.h | 14 +++++++- .../hazelcast_divided_cache_test.cc | 27 ++++++++------ .../hazelcast_unified_cache_test.cc | 9 +++++ .../http/cache/hazelcast_http_cache/util.h | 10 +++--- 8 files changed, 96 insertions(+), 47 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto index a2fcfc4c2349f..fcb54ac220467 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -7,22 +7,28 @@ package envoy.source.extensions.filters.http.cache; // [#extension: envoy.extensions.http.cache] // Hazelcast Http Cache configuration -// TODO: Mention defaults. message HazelcastHttpCacheConfig { + + // Address for Hazelcast cluster member. + message MemberAddress { + string ip = 1; + int32 port = 2; + } + // Group name of Hazelcast cluster to be connected. string group_name = 1; // Group password of Hazelcast cluster to be connected. string group_password = 2; - // IP address of Hazelcast member to be connected. - string ip = 3; - // Port of Hazelcast member to be connected. - int32 port = 4; + // Only one member address is enough to connect to the cluster but + // providing more than one is recommended. + // By default, 127.0.0.1:5701 will be tried. + repeated MemberAddress addresses = 3; // Application specific name for the cache. Different deployments should // use the same prefix if they want to share the same cache and connect - // to the same Hazelcast cluster. - string app_prefix = 5; + // to the same Hazelcast cluster. Empty string by default. + string app_prefix = 4; // In unified mode, cached responses will be stored as a single entry. // On a range HTTP request, regardless of the request range, all the @@ -34,13 +40,16 @@ message HazelcastHttpCacheConfig { // map partitioned with body_partition_size. On a range request, only // required body partitions are called from the distributed map. This // option causes extra memory usage per partition on the cache. - bool unified = 6; + // False by default. + bool unified = 5; // Body partition size for divided cache. Ignored in unified mode. - int64 body_partition_size = 7; + // At most 64 KB allowed. 2 KB by default. + uint64 body_partition_size = 6; // Maximum allowed body size per response. If insertion for a larger // value than the limit is attempted, the first max_body_size bytes // of the response will be cached and the remaining will be ignored. - int64 max_body_size = 8; + // At most 64 KB for UNIFIED mode, 2 MB for DIVIDED mode. + uint64 max_body_size = 7; } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h index e2f40a8074161..87f3d26b0d75e 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h @@ -12,24 +12,28 @@ namespace HazelcastHttpCache { class ConfigUtil { public: - static uint64_t validPartitionSize(const uint64_t& config_value) { + static uint64_t validPartitionSize(const uint64_t config_value) { return config_value == 0 ? DEFAULT_PARTITION_SIZE : (config_value > MAX_PARTITION_SIZE) ? MAX_PARTITION_SIZE : config_value; } - static uint64_t validMaxBodySize(const uint64_t& config_value) { - return config_value == 0 || (config_value > MAX_BODY_SIZE) ? - MAX_BODY_SIZE : config_value; + static uint64_t validMaxBodySize(const uint64_t config_value, const bool unified) { + uint64_t max_size = unified ? MAX_UNIFIED_BODY_SIZE : MAX_DIVIDED_BODY_SIZE; + return config_value == 0 || (config_value > max_size) ? max_size : config_value; } static hazelcast::client::ClientConfig getClientConfig(const envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig& cache_config) { - // TODO: Add retry config, connection config. Use multiple addresses. hazelcast::client::ClientConfig config; config.getGroupConfig().setName(cache_config.group_name()); - config.getNetworkConfig().addAddress(hazelcast::client::Address(cache_config.ip(), - cache_config.port())); + config.getNetworkConfig().setConnectionAttemptPeriod(5000).setConnectionAttemptLimit(5); + config.getConnectionStrategyConfig().setReconnectMode( + hazelcast::client::config::ClientConnectionStrategyConfig::ReconnectMode::ASYNC); + for (auto &address : cache_config.addresses()) { + config.getNetworkConfig().addAddress(hazelcast::client::Address(address.ip(), + address.port())); + } return config; } @@ -38,11 +42,19 @@ class ConfigUtil { } private: - // TODO: Examine the optimal values for defaults and limits. - static constexpr short WARN_PARTITION_LIMIT = 20; - static constexpr uint64_t DEFAULT_PARTITION_SIZE = 4096; - static constexpr uint64_t MAX_PARTITION_SIZE = DEFAULT_PARTITION_SIZE * 16; - static constexpr uint64_t MAX_BODY_SIZE = MAX_PARTITION_SIZE * 16; + // After this much body partitions stored for a response in DIVIDED mode, + // a suggestion log will be appeared to increase partition size. + static constexpr short WARN_PARTITION_LIMIT = 16; + + // Sizes for each divided body entry. + static constexpr uint64_t DEFAULT_PARTITION_SIZE = 2048; + static constexpr uint64_t MAX_PARTITION_SIZE = DEFAULT_PARTITION_SIZE * 32; + + // Size for total body size of a unified response + static constexpr uint64_t MAX_UNIFIED_BODY_SIZE = MAX_PARTITION_SIZE; + + // Size for total body size of a divided response (at most 32 partitions allowed) + static constexpr uint64_t MAX_DIVIDED_BODY_SIZE = MAX_UNIFIED_BODY_SIZE * 32; }; } // HazelcastHttpCache diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index da5bd7b5e4bbe..9bdd1d27390d7 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -321,7 +321,7 @@ void DividedInsertContext::flushBuffer() { ENVOY_LOG(warn, "Number of body partitions for a response has been reached {} (or more).", ConfigUtil::partitionWarnLimit()); ENVOY_LOG(info, "Having so many partitions might cause performance drop " - "as well as extra memory usage. Consider increasing body" + "as well as extra memory usage. Consider increasing body " "partition size."); } } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index e11cbfa149ab5..4312933c3a062 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -189,20 +189,20 @@ void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, }; std::string HazelcastHttpCache::constructMapName(const std::string& postfix) { - return std::string(cache_config_.app_prefix()) - .append(":") - .append(std::to_string(body_partition_size_)) - .append(":") - .append(postfix); + std::string name(cache_config_.app_prefix()); + if (!unified_) { + name.append(":").append(std::to_string(body_partition_size_)); + } + return name.append("-").append(postfix); } HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) : cache_config_(config), unified_(config.unified()), body_partition_size_(ConfigUtil::validPartitionSize(config.body_partition_size())), - max_body_size_(ConfigUtil::validMaxBodySize(config.max_body_size())) { + max_body_size_(ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())) { body_map_name_ = constructMapName("body"); - header_map_name_ = constructMapName("header"); - response_map_name_ = constructMapName("response"); + header_map_name_ = constructMapName("div-cache"); + response_map_name_ = constructMapName("uni-cache"); }; class HazelcastHttpCacheFactory : public HttpCacheFactory { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 27e808c6c502f..5bef55521b4b7 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -73,8 +73,20 @@ class HazelcastHttpCache : public HttpCache, return signed_key; } + /** + * Creates string keys for body partition entries. + * + * @param key Unsigned hash key for the header. + * @param order Order of the body among other partitions starting from 0. + * @return Body partition key for header and order pair. + * + * @note Appending '#' or any other marker between the key and order + * string is required. Otherwise, for instance, the 11th order + * body for key 1 and the 1st order body for key 11 will have + * the same map key "111". + */ inline std::string orderedMapKey(const uint64_t& key, const uint64_t& order) { - return std::to_string(mapKey(key)) + std::to_string(order); + return std::to_string(key).append("#").append(std::to_string(order)); } ~HazelcastHttpCache() { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index ac4e6d03be28e..2298b2b843a1c 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -260,17 +260,22 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { } TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { - const std::string RequestPath("/header/only/response"); - LookupContextPtr name_lookup_context = lookup(RequestPath); - uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - insert(move(name_lookup_context), getResponseHeaders(), ""); - name_lookup_context = lookup(RequestPath); - EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - auto header = testHeaderMap().get(mapKey(variant_hash_key)); - EXPECT_NE(nullptr, header); - EXPECT_EQ(0, header->bodySize()); + auto headerOnlyTest = [this](std::string path, bool empty_body) { + LookupContextPtr name_lookup_context = lookup(path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + insert(move(name_lookup_context), getResponseHeaders(), empty_body ? "" : nullptr); + name_lookup_context = lookup(path); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + EXPECT_EQ(0, lookup_result_.content_length_); + }; + + // This will pass end_stream = true during header insertion. + headerOnlyTest("/header/only/response", false); + + // This will pass end_stream = false during header insertion, + // then empty body for body insertion. + headerOnlyTest("/empty/body/response", true); + EXPECT_EQ(0, testBodyMap().size()); clearMaps(); } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 65b4e240f0aa0..eb392614a28d7 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -140,6 +140,15 @@ TEST_F(HazelcastUnifiedCacheTest, DoNotOverrideExistingResponse) { clearMaps(); } +TEST_F(HazelcastUnifiedCacheTest, HeaderOnlyResponse) { + InsertContextPtr inserter = hz_cache_->makeInsertContext(lookup("/header/only")); + inserter->insertHeaders(getResponseHeaders(), true); + LookupContextPtr name_lookup_context = lookup("/header/only"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + EXPECT_EQ(0, lookup_result_.content_length_); + clearMaps(); +} + TEST_F(HazelcastUnifiedCacheTest, MissLookupOnDifferentKey) { const std::string RequestPath("/miss/on/different/key"); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h index adb5ee46be60a..dc12acc77dc0b 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -11,7 +11,7 @@ class HazelcastTestUtil { public: static constexpr int TEST_PARTITION_SIZE = 10; - static constexpr int TEST_MAX_BODY_SIZE = TEST_PARTITION_SIZE * 10; + static constexpr int TEST_MAX_BODY_SIZE = TEST_PARTITION_SIZE * 20; static const std::string& abortedBodyResponse() { @@ -23,8 +23,9 @@ class HazelcastTestUtil { HazelcastHttpCacheConfig hc; hc.set_group_name("dev"); hc.set_group_password("dev-pass"); - hc.set_ip("127.0.0.1"); - hc.set_port(5701); + HazelcastHttpCacheConfig::MemberAddress* memberAddress = hc.add_addresses(); + memberAddress->set_ip("127.0.0.1"); + memberAddress->set_port(5701); hc.set_body_partition_size(TEST_PARTITION_SIZE); hc.set_app_prefix("test"); hc.set_unified(unified); @@ -67,7 +68,8 @@ class HazelcastHttpCacheTestBase : public testing::Test { void insert(LookupContextPtr lookup, const Http::TestResponseHeaderMapImpl& response_headers, const absl::string_view response_body) { InsertContextPtr inserter = hz_cache_->makeInsertContext(move(lookup)); - inserter->insertHeaders(response_headers, false); + inserter->insertHeaders(response_headers, response_body == nullptr); + if (response_body == nullptr) return; inserter->insertBody(Buffer::OwnedImpl(response_body), nullptr, true); } From f359a8e499e11e67662c0567324364318dd81da4 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Mon, 30 Mar 2020 18:43:45 +0300 Subject: [PATCH 03/33] Use Envoy's random for versioning. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_http_cache/BUILD | 1 + .../hazelcast_http_cache/hazelcast_context.cc | 18 +++++- .../hazelcast_http_cache/hazelcast_context.h | 56 ++++++++++++------- .../hazelcast_http_cache.cc | 5 ++ .../hazelcast_http_cache.h | 6 ++ .../hazelcast_divided_cache_test.cc | 21 +++++++ 6 files changed, 86 insertions(+), 21 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD index ea9573a3fd082..27aa7ab936267 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -26,6 +26,7 @@ envoy_cc_extension( "//source/common/buffer:buffer_lib", "//source/common/common:logger_lib", "//source/common/http:header_map_lib", + "//source/common/runtime:runtime_lib", "//include/envoy/registry", "//source/extensions/filters/http/cache:http_cache_lib", ], diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index 9bdd1d27390d7..ff14560d492f3 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -141,18 +141,19 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { return; } if (header_entry) { + abort_insertion_ = true; // overriding an exiting entry is not allowed. ENVOY_LOG(debug, "Found divided response for key {}u, version {}, body size = {}", variant_hash_key_, header_entry->version(), header_entry->bodySize()); if (!MessageDifferencer::Equals(header_entry->variantKey(), variantKey())) { // The same logic with UnifiedLookupContext#getHeaders applies. ENVOY_LOG(debug, "Keys mismatched for hash {}u. " "Aborting lookup & insertion", variant_hash_key_); - abort_insertion_ = true; cb(LookupResult{}); return; } this->total_body_size_ = header_entry->bodySize(); this->version_ = header_entry->version(); + this->found_header_ = true; cb(lookup_request_.makeLookupResult(std::move(header_entry->headerMap()), total_body_size_)); } else { ENVOY_LOG(debug, "Didn't find divided response for key {}u", variant_hash_key_); @@ -188,7 +189,7 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { // void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) { ASSERT(range.end() <= total_body_size_); - ASSERT(!abort_insertion_); + ASSERT(found_header_, "Header lookup is missed."); // Lookup for only one body partition which includes the range.begin(). uint64_t body_index = range.begin() / body_partition_size_; @@ -347,6 +348,19 @@ void DividedInsertContext::flushHeader() { } } +int32_t DividedInsertContext::createVersion() { + // We do not need a strong uniformity or randomness here. Even + // the versions of two different header entries with distinct + // hash keys are the same, this will not cause a problem at all. + // We only need a stamp for bodies which inserted for this context. + // Since this version is stored in cache entries, 32 bit random + // derived from the 64 bit is preferred here. + // Range: from (int32.MIN + 1) to (int32.MAX - 1), inclusive. + uint64_t rand64 = hz_cache_.random(); + uint64_t max = std::numeric_limits::max(); + return (rand64 % (max * 2)) - max; +} + } // HazelcastHttpCache } // Cache } // HttpFilters diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h index b6b67332964b2..47384ed3b4572 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h @@ -57,11 +57,6 @@ class HazelcastLookupContextBase : public LookupContext, * * @param raw_key Key created by the filter. */ - // TODO(enozcan): Ensure uniqueness of hash key for the same same response. - // Different hash keys will be created if the order of values differ for the same - // vary header key. Hence the response will not be affected but the same response - // will be cached with different keys. (might be reordering/sorting here). - // Prevent str copies as possible. void createVariantKey(Key& raw_key) { ASSERT(raw_key.custom_fields_size() == 0); ASSERT(raw_key.custom_ints_size() == 0); // Key must be pure. @@ -74,12 +69,38 @@ class HazelcastLookupContextBase : public LookupContext, header_strings.push_back(std::make_pair(std::string(header.key().getStringView()), std::string(header.value().getStringView()))); } + + // Different order of headers causes different hash keys even if their both key and value + // are the same. That is, the following two header lists will cause different hashes for + // the same response and hence they are sorted before insertion. + // + // { {"User-Agent", "desktop"}, {"Accept-Encoding","gzip"} } + // { {"Accept-Encoding","gzip"}, {"User-Agent", "desktop"} } + std::sort(header_strings.begin(), header_strings.end(), [](auto& left, auto& right) -> bool { - return left.first == right.first ? left.second < right.second : left.first < right.first; + // Per https://tools.ietf.org/html/rfc2616#section-4.2 if two different header entries + // have the same field-name, then their order should not change. For distinct field-named + // headers the order is not significant but sorted alphabetically here to get the same hash + // for the same headers. + return left.first == right.first ? false : left.first < right.first; }); + for (auto& header : header_strings) { - raw_key.add_custom_fields(header.first + ":" + header.second); + raw_key.add_custom_fields(std::move(header.first)); + raw_key.add_custom_fields(std::move(header.second)); } + // stableHashKey now creates variant hash for the key since its custom_fields are like: + // [ "Accept-Encoding", "gzip", "User-Agent", "desktop"] + + // TODO(enozcan): Ensure the generation of the same key for the same response independent + // from the header orders. + // + // Different hash keys will be created if the order of values differ for the same + // vary header key. The response will not be affected but the same response will + // be cached with different keys. i.e. two different hashes exist for the followings + // where the only allowed vary header is "accept-language": + // - {accept-language: en-US,tr;q=0.8} + // - {accept-language: tr;q=0.8,en-US} } }; @@ -159,6 +180,8 @@ class DividedLookupContext : public HazelcastLookupContextBase { /** Max body size per body entry defined via config. */ const uint64_t body_partition_size_; + + bool found_header_ = false; }; /** @@ -176,18 +199,13 @@ class DividedInsertContext : public HazelcastInsertContextBase { void flushBuffer(); void flushHeader(); - /** Creates a common version for a header and its body entries. - * This version entity secures the relation between a header and - * its bodies hence they are stored in different distributed maps - * in DIVIDED mode. */ - int32_t createVersion(){ - // TODO: Use a secure or Envoy style random. - // The was designed to be the timestamp fetched from the Hazelcast - // cluster during insertion (clusterTime). However, it's currently not - // supported by the cpp client. Might be available in 4.0 release. - // Related issue is: https://github.com/hazelcast/hazelcast-cpp-client/issues/581 - return std::rand(); - } + /** + * Creates a common version for a header and its body entries. + * This version denotes the relation between a header and its + * bodies such that they are inserted in the same insert context + * for the same lookup in DIVIDED mode. + */ + int32_t createVersion(); /** Counter for the order of next body entry to be inserted. */ int body_order_ = 0; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index 4312933c3a062..ba183c3d27bf7 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -31,6 +31,11 @@ void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, Http::ResponseHeaderMapPtr&& response_headers) { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; // TODO(enozcan): Enable when implemented on the filter side. + // Depending on the filter's implementation, the cached entry's + // variant_key_ must be updated as well. + // Also, if vary headers change then the hash key of the response + // will change and updating only header map will not be enough in + // this case. ASSERT(lookup_context); ASSERT(response_headers); if (unified_) { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 5bef55521b4b7..703e0d8db9e96 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -1,6 +1,7 @@ #pragma once #include "common/common/logger.h" +#include "common/runtime/runtime_impl.h" #include "extensions/filters/http/cache/http_cache.h" #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" @@ -89,6 +90,10 @@ class HazelcastHttpCache : public HttpCache, return std::to_string(key).append("#").append(std::to_string(order)); } + uint64_t random() { + return rand_.random(); + } + ~HazelcastHttpCache() { shutdown(); }; @@ -132,6 +137,7 @@ class HazelcastHttpCache : public HttpCache, std::string header_map_name_; std::string response_map_name_; + Runtime::RandomGeneratorImpl rand_; }; } // namespace HazelcastHttpCache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 2298b2b843a1c..30d36b66df449 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -101,6 +101,27 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { clearMaps(); } +TEST_F(HazelcastDividedCacheTest, PreventOverridingCacheEntries) { + const std::string RequestPath("/prevent/override/cached/response"); + + LookupContextPtr name_lookup_context = lookup(RequestPath); + const std::string OriginalBody(HazelcastTestUtil::TEST_PARTITION_SIZE * 2, 'h'); + insert(move(name_lookup_context), getResponseHeaders(), OriginalBody); + + name_lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + + // A possible call to insertion below is filter's fault, not an expected behavior. + const std::string OverriddenBody(HazelcastTestUtil::TEST_PARTITION_SIZE * 3, 'z'); + insert(move(name_lookup_context), getResponseHeaders(), OverriddenBody); + + name_lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), OriginalBody)); + EXPECT_EQ(2, testBodyMap().size()); + EXPECT_EQ(1, testHeaderMap().size()); + clearMaps(); +} + TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { const std::string RequestPath("/only/one/must/insert"); From d6f2568850b8985b8b74a218cc6ad85d6b19a739 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Wed, 1 Apr 2020 17:26:32 +0300 Subject: [PATCH 04/33] Handle all connection failures. Use parameterized test for common ones. Signed-off-by: Enes Ozcan --- .../cache/hazelcast_http_cache/config.proto | 35 +- .../cache/hazelcast_http_cache/config_util.h | 31 +- .../hazelcast_http_cache/hazelcast_context.cc | 121 ++++-- .../hazelcast_http_cache/hazelcast_context.h | 16 +- .../hazelcast_http_cache.cc | 48 ++- .../hazelcast_http_cache.h | 6 +- .../http/cache/hazelcast_http_cache/BUILD | 12 + .../hazelcast_common_cache_test.cc | 215 +++++++++++ .../hazelcast_divided_cache_test.cc | 350 +++++------------- .../hazelcast_unified_cache_test.cc | 277 +++----------- .../http/cache/hazelcast_http_cache/util.h | 202 +++++----- 11 files changed, 676 insertions(+), 637 deletions(-) create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto index fcb54ac220467..d74d2efcd537e 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -15,20 +15,43 @@ message HazelcastHttpCacheConfig { int32 port = 2; } - // Group name of Hazelcast cluster to be connected. + // Group name of Hazelcast cluster to be connected. Not only the address of a member + // but its group name must match. string group_name = 1; // Group password of Hazelcast cluster to be connected. string group_password = 2; + // The timeout value in milliseconds for Hazelcast members to accept this client's + // connection requests. If the member does not respond within the timeout, the client + // will think the connection is lost and retry to connect to cluster as many as + // connection_attempt_limit. 5000 by default and 0 is not allowed. + uint32 connection_timeout = 3; + + // When the client connection to the cluster is down, client will retry as many + // as connection_attempt_limit before giving up. After this much of retries, the + // client will go offline and cache will not be active from then on permanently. + // 10 by default and 0 is not allowed. + uint32 connection_attempt_limit = 4; + + // The duration in milliseconds between the connection attempts to cluster. + // 5000 by default and 0 is not allowed. + uint32 connection_attempt_period = 5; + + // The timeout value in seconds for a call to be responded by Hazelcast cluster. + // If a member does not respond within the timeout, the lookup or insert operation + // will be cancelled and treated as a cache miss or an aborted insertion. + // 8 by default and 0 is not allowed. + uint32 invocation_timeout = 6; + // Only one member address is enough to connect to the cluster but // providing more than one is recommended. // By default, 127.0.0.1:5701 will be tried. - repeated MemberAddress addresses = 3; + repeated MemberAddress addresses = 7; // Application specific name for the cache. Different deployments should // use the same prefix if they want to share the same cache and connect // to the same Hazelcast cluster. Empty string by default. - string app_prefix = 4; + string app_prefix = 8; // In unified mode, cached responses will be stored as a single entry. // On a range HTTP request, regardless of the request range, all the @@ -41,15 +64,15 @@ message HazelcastHttpCacheConfig { // required body partitions are called from the distributed map. This // option causes extra memory usage per partition on the cache. // False by default. - bool unified = 5; + bool unified = 9; // Body partition size for divided cache. Ignored in unified mode. // At most 64 KB allowed. 2 KB by default. - uint64 body_partition_size = 6; + uint64 body_partition_size = 10; // Maximum allowed body size per response. If insertion for a larger // value than the limit is attempted, the first max_body_size bytes // of the response will be cached and the remaining will be ignored. // At most 64 KB for UNIFIED mode, 2 MB for DIVIDED mode. - uint64 max_body_size = 7; + uint64 max_body_size = 11; } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h index 87f3d26b0d75e..64447e012e5e0 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h @@ -27,13 +27,21 @@ class ConfigUtil { envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig& cache_config) { hazelcast::client::ClientConfig config; config.getGroupConfig().setName(cache_config.group_name()); - config.getNetworkConfig().setConnectionAttemptPeriod(5000).setConnectionAttemptLimit(5); + config.getNetworkConfig().setConnectionTimeout(cache_config.connection_timeout() + == 0 ? DEFAULT_CONNECTION_TIMEOUT_MS : cache_config.connection_timeout()); + config.getNetworkConfig().setConnectionAttemptLimit(cache_config.connection_attempt_limit() + == 0 ? DEFAULT_CONNECTION_ATTEMPT_LIMIT : cache_config.connection_attempt_limit()); + config.getNetworkConfig().setConnectionAttemptPeriod(cache_config.connection_attempt_period() + == 0 ? DEFAULT_CONNECTION_ATTEMPT_PERIOD_MS : cache_config.connection_attempt_period()); config.getConnectionStrategyConfig().setReconnectMode( - hazelcast::client::config::ClientConnectionStrategyConfig::ReconnectMode::ASYNC); + hazelcast::client::config::ClientConnectionStrategyConfig::ReconnectMode::ASYNC); for (auto &address : cache_config.addresses()) { config.getNetworkConfig().addAddress(hazelcast::client::Address(address.ip(), - address.port())); + address.port())); } + config.setProperty("hazelcast.client.invocation.timeout.seconds", + std::to_string(cache_config.invocation_timeout() == 0 ? + DEFAULT_INVOCATION_TIMEOUT_SEC : cache_config.invocation_timeout())); return config; } @@ -48,13 +56,26 @@ class ConfigUtil { // Sizes for each divided body entry. static constexpr uint64_t DEFAULT_PARTITION_SIZE = 2048; + static constexpr uint64_t MAX_PARTITION_SIZE = DEFAULT_PARTITION_SIZE * 32; - // Size for total body size of a unified response + // Size for total body size of a unified response. static constexpr uint64_t MAX_UNIFIED_BODY_SIZE = MAX_PARTITION_SIZE; - // Size for total body size of a divided response (at most 32 partitions allowed) + // Size for total body size of a divided response (at most 32 partitions allowed). static constexpr uint64_t MAX_DIVIDED_BODY_SIZE = MAX_UNIFIED_BODY_SIZE * 32; + + // Duration to try to reconnect a cluster if a member does not respond. + static constexpr uint32_t DEFAULT_CONNECTION_TIMEOUT_MS = 5000; + + // Limit of connection attempts before go offline. + static constexpr uint32_t DEFAULT_CONNECTION_ATTEMPT_LIMIT = 10; + + // Duration between connection retries. + static constexpr uint32_t DEFAULT_CONNECTION_ATTEMPT_PERIOD_MS = 5000; + + // Duration for an invocation to be cancelled. + static constexpr uint32_t DEFAULT_INVOCATION_TIMEOUT_SEC = 8; }; } // HazelcastHttpCache diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index ff14560d492f3..57953ed1fbc64 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -19,10 +19,14 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { try { response_ = hz_cache_.getResponse(variant_hash_key_); } catch (HazelcastClientOfflineException e){ - ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" - "until the connection is restored..."); - abort_insertion_ = true; - cb(LookupResult{}); + handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " + "insertions until the connection is restored...", cb); + return; + } catch (OperationTimeoutException e) { + handleLookupFailure("Operation timed out during cache lookup.", cb); + return; + } catch (...) { + handleLookupFailure("Lookup to cache has failed.", cb); return; } if (response_) { @@ -33,10 +37,8 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { // is performed here. If a different response is found with the same // hash (probably on hash collisions), the new response is denied to // be cached and the old one remains. - ENVOY_LOG(debug, "Keys mismatched for hash {}u. " - "Aborting lookup & insertion", variant_hash_key_); - abort_insertion_ = true; - cb(LookupResult{}); + handleLookupFailure("Mismatched keys found for unsigned hash: " + + std::to_string(variant_hash_key_), cb, false); return; } cb(lookup_request_.makeLookupResult(std::move(response_->header().headerMap()), @@ -121,7 +123,12 @@ void UnifiedInsertContext::flushEntry() { try { hz_cache_.putResponseIfAbsent(variant_hash_key_, entry); } catch (HazelcastClientOfflineException e) { - ENVOY_LOG(warn, "Hazelcast cluster connection is lost!"); } + ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Failed to insert response."); + } catch (OperationTimeoutException e) { + ENVOY_LOG(warn, "Operation timed out during cache insertion."); + } catch (...) { + ENVOY_LOG(warn, "Response insertion to cache has failed."); + } } DividedLookupContext::DividedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request) @@ -134,10 +141,14 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { try { header_entry = hz_cache_.getHeader(variant_hash_key_); } catch (HazelcastClientOfflineException e){ - ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" - "until the connection is restored..."); - abort_insertion_ = true; - cb(LookupResult{}); + handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " + "insertions until the connection is restored.", cb); + return; + } catch (OperationTimeoutException e) { + handleLookupFailure("Operation timed out during cache lookup.", cb); + return; + } catch (...) { + handleLookupFailure("Lookup to cache has failed.", cb); return; } if (header_entry) { @@ -145,10 +156,8 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { ENVOY_LOG(debug, "Found divided response for key {}u, version {}, body size = {}", variant_hash_key_, header_entry->version(), header_entry->bodySize()); if (!MessageDifferencer::Equals(header_entry->variantKey(), variantKey())) { - // The same logic with UnifiedLookupContext#getHeaders applies. - ENVOY_LOG(debug, "Keys mismatched for hash {}u. " - "Aborting lookup & insertion", variant_hash_key_); - cb(LookupResult{}); + handleLookupFailure("Mismatched keys found for unsigned hash: " + + std::to_string(variant_hash_key_), cb, false); return; } this->total_body_size_ = header_entry->bodySize(); @@ -166,9 +175,12 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { try { abort_insertion_ = !hz_cache_.tryLock(variant_hash_key_); } catch (HazelcastClientOfflineException e) { - ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" - " until the connection is restored..."); - abort_insertion_ = true; + handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and insertions" + " until the connection is restored...", cb); + return; + } catch (...) { + handleLookupFailure("Lock trial has failed.", cb); + return; } cb(LookupResult{}); } @@ -197,9 +209,14 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal try { body = hz_cache_.getBody(variant_hash_key_, body_index); } catch (HazelcastClientOfflineException e) { - ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions" - " until the connection is restored..."); - cb(nullptr); + handleBodyLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " + "insertions until the connection is restored...", cb); + return; + } catch (OperationTimeoutException e) { + handleBodyLookupFailure("Operation timed out during cache lookup.", cb); + return; + } catch (...) { + handleBodyLookupFailure("Lookup to cache for body entry has failed.", cb); return; } @@ -207,10 +224,10 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal ENVOY_LOG(debug, "Found divided body with key {}u + \"{}\", version {}, size {}", variant_hash_key_, body_index, body->version(),body->length()); if (body->version() != version_) { - ENVOY_LOG(debug, "Body version mismatched with header for key {}u at body: {}. " - "Aborting lookup and performing cleanup.", variant_hash_key_, body_index); hz_cache_.onVersionMismatch(variant_hash_key_, version_, total_body_size_); - cb(nullptr); + handleBodyLookupFailure(fmt::format("Body version mismatched with header for " + "key {}u at body: {}. Aborting lookup and performing cleanup.", variant_hash_key_, + body_index), cb, false); return; } uint64_t offset = (range.begin() % body_partition_size_); @@ -226,13 +243,23 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal } } else { // Body partition is expected to reside in the cache but lookup is failed. - ENVOY_LOG(debug, "Found missing body for key {}u at body: {}. Cleaning up response" - "with body size: {}", variant_hash_key_, body_index, total_body_size_); hz_cache_.onMissingBody(variant_hash_key_, version_, total_body_size_); - cb(nullptr); + handleBodyLookupFailure(fmt::format("Found missing body for key {}u at index: {}. Response " + "with body size {} has been cleaned up from the cache.",variant_hash_key_, body_index, + total_body_size_), cb, false); } }; +void DividedLookupContext::handleBodyLookupFailure(absl::string_view message, + const LookupBodyCallback& cb, bool warn_log){ + if (warn_log) { + ENVOY_LOG(warn, "{}", message); + } else { + ENVOY_LOG(debug, "{}", message); + } + cb(nullptr); +} + DividedInsertContext::DividedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache) : HazelcastInsertContextBase(lookup_context, cache), body_partition_size_(cache.bodySizePerEntry()), version_(createVersion()) {} @@ -276,7 +303,13 @@ void DividedInsertContext::insertBody(const Buffer::Instance& chunk, copyIntoLocalBuffer(copied_bytes, available_bytes, chunk); ASSERT(buffer_vector_.size() == body_partition_size_); remaining_bytes -= available_bytes; - flushBuffer(); + if (!flushBuffer()) { + // Abort insertion if one of the body insertions fails. + if (ready_for_next_chunk) { + ready_for_next_chunk(false); + } + return; + } } else { // Copy all the bytes starting from chunk[copied_bytes] into buffer. Current // buffer can hold the remaining data. @@ -288,8 +321,12 @@ void DividedInsertContext::insertBody(const Buffer::Instance& chunk, if (end_stream || trimmed) { // Header shouldn't be inserted before body insertions are completed. // Total body size in the header entry is computed via inserted body partitions. - flushBuffer(); - flushHeader(); + if (flushBuffer()) { + // Header insertion is performed only when all bodies are stored. + // Otherwise, insertion will be aborted and another insert context + // will store the response by overriding body entries flushed so far. + flushHeader(); + } } if (ready_for_next_chunk) { ready_for_next_chunk(!trimmed); @@ -304,10 +341,16 @@ void DividedInsertContext::copyIntoLocalBuffer(uint64_t& offset, uint64_t& size, offset += size; }; -void DividedInsertContext::flushBuffer() { +/** + * Wraps the current body buffer with HazelcastBodyEntry and puts + * into the cache. + * + * @return True if insertion is completed. + */ +bool DividedInsertContext::flushBuffer() { ASSERT(!abort_insertion_); if (buffer_vector_.size() == 0) { - return; + return true; } total_body_size_ += buffer_vector_.size(); HazelcastBodyEntry bodyEntry(hz_cache_.mapKey(variant_hash_key_), @@ -317,6 +360,13 @@ void DividedInsertContext::flushBuffer() { hz_cache_.putBody(variant_hash_key_, body_order_++, bodyEntry); } catch (HazelcastClientOfflineException e) { ENVOY_LOG(warn, "Hazelcast cluster connection is lost!"); + return false; + } catch (OperationTimeoutException e) { + ENVOY_LOG(warn, "Operation timed out during body insertion."); + return false; + } catch (...) { + ENVOY_LOG(warn, "Body insertion to cache has failed."); + return false; } if (body_order_ == ConfigUtil::partitionWarnLimit()) { ENVOY_LOG(warn, "Number of body partitions for a response has been reached {} (or more).", @@ -325,6 +375,7 @@ void DividedInsertContext::flushBuffer() { "as well as extra memory usage. Consider increasing body " "partition size."); } + return true; } void DividedInsertContext::flushHeader() { @@ -345,6 +396,8 @@ void DividedInsertContext::flushHeader() { // option can be used when available in a future release of cpp client. The related // issue can be tracked at: https://github.com/hazelcast/hazelcast-cpp-client/issues/579 // TODO(enozcan): Use tryLock with leaseTime when released for Hazelcast cpp client. + } catch (...) { + ENVOY_LOG(warn, "Failed to complete response insertion."); } } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h index 47384ed3b4572..eb3505de2f827 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h @@ -35,6 +35,17 @@ class HazelcastLookupContextBase : public LookupContext, protected: + void handleLookupFailure(absl::string_view message, const LookupHeadersCallback& cb, + bool warn_log = true) { + if (warn_log) { + ENVOY_LOG(warn, "{}", message); + } else { + ENVOY_LOG(debug, "{}", message); + } + abort_insertion_ = true; + cb(LookupResult{}); + } + HazelcastHttpCache& hz_cache_; LookupRequest lookup_request_; @@ -174,6 +185,9 @@ class DividedLookupContext : public HazelcastLookupContextBase { void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; private: + void handleBodyLookupFailure(absl::string_view message, const LookupBodyCallback& cb, + bool warn_log = true); + uint64_t total_body_size_; int32_t version_; @@ -196,7 +210,7 @@ class DividedInsertContext : public HazelcastInsertContextBase { private: void copyIntoLocalBuffer(uint64_t& index, uint64_t& size, const Buffer::Instance& source); - void flushBuffer(); + bool flushBuffer(); void flushHeader(); /** diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index ba183c3d27bf7..61e8a424f4655 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -38,10 +38,18 @@ void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, // this case. ASSERT(lookup_context); ASSERT(response_headers); - if (unified_) { - updateUnifiedHeaders(std::move(lookup_context), std::move(response_headers)); - } else { - updateDividedHeaders(std::move(lookup_context), std::move(response_headers)); + try { + if (unified_) { + updateUnifiedHeaders(std::move(lookup_context), std::move(response_headers)); + } else { + updateDividedHeaders(std::move(lookup_context), std::move(response_headers)); + } + } catch (HazelcastClientOfflineException e) { + ENVOY_LOG(warn, "Hazelcast Connection is offline!"); + } catch (OperationTimeoutException e) { + ENVOY_LOG(warn, "Updating headers has timed out."); + } catch (...) { + ENVOY_LOG(warn, "Updating headers has failed."); } } @@ -83,7 +91,7 @@ HazelcastResponsePtr HazelcastHttpCache::getResponse(const uint64_t& key) { } void HazelcastHttpCache::connect() { - if (hazelcast_client_) { + if (hazelcast_client_ && hazelcast_client_->getLifecycleService().isRunning()) { ENVOY_LOG(warn, "Client is already connected. Cluster name: {}", hazelcast_client_->getClientConfig().getGroupConfig().getName()); return; @@ -108,12 +116,20 @@ void HazelcastHttpCache::connect() { " from the map named {}.", unified_ ? response_map_name_ : header_map_name_); } -void HazelcastHttpCache::shutdown() { - if (hazelcast_client_) { +void HazelcastHttpCache::shutdown(bool destroy) { + if (!hazelcast_client_) { + ENVOY_LOG(warn, "Client is already offline."); + return; + } + if (hazelcast_client_->getLifecycleService().isRunning()) { ENVOY_LOG(info, "Shutting down Hazelcast connection..."); hazelcast_client_->shutdown(); - hazelcast_client_.release(); ENVOY_LOG(info, "Cache is offline now."); + } else { + ENVOY_LOG(warn, "Cache is already offline."); + } + if (destroy) { + hazelcast_client_.reset(); } } @@ -141,8 +157,12 @@ void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t } catch (HazelcastClientOfflineException e) { // see DividedInsertContext#flushHeader() for left over locks on a connection failure. ENVOY_LOG(warn, "Hazelcast Connection is offline!"); + } catch (OperationTimeoutException e) { + ENVOY_LOG(warn, "Clean up for missing body has timed out."); + } catch (...) { + ENVOY_LOG(warn, "Clean up for missing body has failed."); } -}; +} void HazelcastHttpCache::onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) { onMissingBody(key, version, body_size); @@ -163,6 +183,10 @@ void HazelcastHttpCache::unlock(const uint64_t& key) { } } +HazelcastHttpCache::~HazelcastHttpCache() { + shutdown(true); +} + void HazelcastHttpCache::updateUnifiedHeaders(LookupContextPtr&& lookup_context, Http::ResponseHeaderMapPtr&& response_headers) { const uint64_t& key = static_cast(lookup_context.get())->variantHashKey(); @@ -176,7 +200,7 @@ void HazelcastHttpCache::updateUnifiedHeaders(LookupContextPtr&& lookup_context, updated.header().headerMap(std::move(response_headers)); // Update headers if no other update is performed in meantime. getResponseMap().replace(key, updated, *response); -}; +} void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, Http::ResponseHeaderMapPtr&& response_headers) { @@ -191,7 +215,7 @@ void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, updated.headerMap(std::move(response_headers)); // Update headers if no other update is performed in meantime. getHeaderMap().replace(key, updated, *stale); -}; +} std::string HazelcastHttpCache::constructMapName(const std::string& postfix) { std::string name(cache_config_.app_prefix()); @@ -208,7 +232,7 @@ HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) body_map_name_ = constructMapName("body"); header_map_name_ = constructMapName("div-cache"); response_map_name_ = constructMapName("uni-cache"); -}; +} class HazelcastHttpCacheFactory : public HttpCacheFactory { public: diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 703e0d8db9e96..2fb738716ec3f 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -49,7 +49,7 @@ class HazelcastHttpCache : public HttpCache, // Hazelcast cluster connection void connect(); - void shutdown(); + void shutdown(bool destroy); // Recoveries for malformed entries void onMissingBody(uint64_t key, int32_t version, uint64_t body_size); @@ -94,9 +94,7 @@ class HazelcastHttpCache : public HttpCache, return rand_.random(); } - ~HazelcastHttpCache() { - shutdown(); - }; + ~HazelcastHttpCache(); private: friend class HazelcastHttpCacheTestBase; diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 5805f6770ae99..7810082c1ba9c 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -31,6 +31,18 @@ envoy_extension_cc_test( ], ) +envoy_extension_cc_test( + name = "hazelcast_common_cache_test", + srcs = ["hazelcast_common_cache_test.cc"], + extension_name = "envoy.filters.http.cache.hazelcast_http_cache", + deps = [ + ":hazelcast_test_util_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) + + envoy_extension_cc_test_library( name = "hazelcast_test_util_lib", hdrs = ["util.h"], diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc new file mode 100644 index 0000000000000..cc4d949a8d20f --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -0,0 +1,215 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "envoy/registry/registry.h" + +#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +/** + * Common tests for both DIVIDED and UNIFIED cache mode. + */ +class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, + public testing::WithParamInterface { +protected: + + void SetUp() { + HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(GetParam()); + cache_ = new HazelcastHttpCache(cfg); + cache_->connect(); + clearMaps(); + } +}; + +INSTANTIATE_TEST_SUITE_P(CommonCacheTests, HazelcastHttpCacheTest, ::testing::Bool()); + +TEST_P(HazelcastHttpCacheTest, MissPutAndGetEntries) { + const std::string RequestPath1("/size/plus/one"); + const std::string RequestPath2("/exactly/size"); + const std::string RequestPath3("/size/minus/one"); + + LookupContextPtr lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr lookup_context3 = lookup(RequestPath3); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + int length = HazelcastTestUtil::TEST_PARTITION_SIZE * 2; + const std::string Body1("s", length + 1); + absl::string_view Body2(Body1.c_str(), length); + absl::string_view Body3(Body1.c_str(), length - 1); + + insert(move(lookup_context1), getResponseHeaders(), Body1); + insert(move(lookup_context2), getResponseHeaders(), Body2); + insert(move(lookup_context3), getResponseHeaders(), Body3); + + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath1).get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath3).get(), Body3)); +} + +TEST_P(HazelcastHttpCacheTest, HandleRangedResponses) { + const std::string RequestPath("/ranged/responses"); + + LookupContextPtr lookup_context1 = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + int size = HazelcastTestUtil::TEST_PARTITION_SIZE; + const std::string Body1 = std::string(size, 'h'); + const std::string Body2 = std::string(size, 'z'); + const std::string Body3 = std::string(size, 'c'); + const std::string Body = Body1 + Body2 + Body3; + + insert(move(lookup_context1), getResponseHeaders(), Body); + lookup_context1 = lookup(RequestPath); + + // 'h' * (size) + EXPECT_EQ(absl::string_view(Body.c_str(), size), + getBody(*lookup_context1, 0, size)); + + // 'z' * (size) + EXPECT_EQ(absl::string_view(Body.c_str() + size, size), + getBody(*lookup_context1, size, size * 2)); + + // 'h' * (size/2) + 'z' * (size/2) + EXPECT_EQ(absl::string_view(Body.c_str() + size / 2, size), + getBody(*lookup_context1, size / 2, size + size / 2)); + + // 'h' + 'z' * (size) + 'c' + EXPECT_EQ(absl::string_view(Body.c_str() + size - 1, size + 2), + getBody(*lookup_context1, size - 1, 2 * size + 1)); + + // 'h' * (size) + 'z' * (size) + 'c' * (size) + EXPECT_EQ(absl::string_view(Body.c_str(), size * 3), + getBody(*lookup_context1, 0, size * 3)); +} + +// +// Tests belong to SimpleHttpCache are applied below with minor changes on the test body. +// +TEST_P(HazelcastHttpCacheTest, SimplePutGet) { + const std::string RequestPath1("/simple/put/first"); + LookupContextPtr name_lookup_context = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body1("hazelcast"); + insert(move(name_lookup_context), getResponseHeaders(), Body1); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath1).get(), Body1)); + + const std::string RequestPath2("/simple/put/second"); + name_lookup_context = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body2("hazelcast.http.cache"); + insert(move(name_lookup_context), getResponseHeaders(), Body2); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); +} + +TEST_P(HazelcastHttpCacheTest, PrivateResponse) { + const std::string request_path("/private/response"); + + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + const std::string Body("Value"); + + insert(move(name_lookup_context), getResponseHeaders(), Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); +} + +TEST_P(HazelcastHttpCacheTest, Miss) { + LookupContextPtr name_lookup_context = lookup("/no/such/entry"); + uint64_t variant_hash_key = + static_cast(*name_lookup_context).variantHashKey(); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // Do not left over a missed lookup without inserting or releasing its lock. + unlockKey(variant_hash_key); +} + +TEST_P(HazelcastHttpCacheTest, Fresh) { + insert("/", getResponseHeaders(), ""); + time_source_.sleep(std::chrono::seconds(3600)); + lookup("/"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); +} + +TEST_P(HazelcastHttpCacheTest, Stale) { + insert("/", getResponseHeaders(), ""); + time_source_.sleep(std::chrono::seconds(3601)); + lookup("/"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); +} + +TEST_P(HazelcastHttpCacheTest, RequestSmallMinFresh) { + request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "min-fresh=1000"); + const std::string request_path("/request/small/min/fresh"); + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + Http::TestResponseHeaderMapImpl response_headers{ + {"date", formatter_.fromTime(current_time_)}, + {"age", "6000"}, + {"cache-control", "public, max-age=9000"}}; + const std::string Body("content"); + insert(move(name_lookup_context), response_headers, Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); +} + +TEST_P(HazelcastHttpCacheTest, ResponseStaleWithRequestLargeMaxStale) { + request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "max-stale=9000"); + + const std::string request_path("/response/stale/with/request/large/max/stale"); + LookupContextPtr name_lookup_context = lookup(request_path); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + Http::TestResponseHeaderMapImpl response_headers{ + {"date", formatter_.fromTime(current_time_)}, + {"age", "7200"}, + {"cache-control", "public, max-age=3600"}}; + + const std::string Body("content"); + insert(move(name_lookup_context), response_headers, Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); +} + +TEST_P(HazelcastHttpCacheTest, StreamingPutAndRangeGet) { + InsertContextPtr inserter = makeInsertContext("/streaming/put"); + inserter->insertHeaders(getResponseHeaders(), false); + inserter->insertBody( + Buffer::OwnedImpl("Hello, "), + [](bool ready){EXPECT_TRUE(ready); }, false); + inserter->insertBody(Buffer::OwnedImpl("World!"), nullptr, true); + LookupContextPtr name_lookup_context = lookup("/streaming/put"); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + EXPECT_NE(nullptr, lookup_result_.headers_); + ASSERT_EQ(13, lookup_result_.content_length_); + EXPECT_EQ("Hello, World!", getBody(*name_lookup_context, 0, 13)); + EXPECT_EQ("o, World!", getBody(*name_lookup_context, 4, 13)); +} + +TEST(Registration, GetFactory) { + HttpCacheFactory* factory = Registry::FactoryRegistry::getFactoryByType( + "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); + ASSERT_NE(factory, nullptr); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig config; + HazelcastHttpCacheConfig hz_cache_config = HazelcastTestUtil::getTestConfig(true); + config.mutable_typed_config()->PackFrom(hz_cache_config); + HazelcastHttpCache& cache = static_cast(factory->getCache(config)); + EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); + + // Explicitly destroy Hazelcast connection here. Otherwise the test + // environment does not wait for cache destructor to be completed + // and this causes segfault when Hazelcast Client is shutting down. + cache.shutdown(true); +} + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 30d36b66df449..13e50941355e4 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -1,8 +1,4 @@ -#include "test/test_common/simulated_time_system.h" -#include "test/test_common/utility.h" #include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" - -#include "envoy/registry/registry.h" #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" namespace Envoy { @@ -11,165 +7,103 @@ namespace HttpFilters { namespace Cache { namespace HazelcastHttpCache { +/** + * Tests for DIVIDED cache mode. + */ class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { protected: - static void SetUpTestSuite() { + void SetUp() { HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(false); - hz_cache_ = new HazelcastHttpCache(cfg); - hz_cache_->connect(); + cache_ = new HazelcastHttpCache(cfg); + cache_->connect(); clearMaps(); } }; -TEST_F(HazelcastDividedCacheTest, SimpleMissAndPutEntries) { - const std::string RequestPath1("/size/minus/one"); - const std::string RequestPath2("/exactly/size"); - const std::string RequestPath3("/size/plus/one"); - - LookupContextPtr name_lookup_context1 = lookup(RequestPath1); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - LookupContextPtr name_lookup_context2 = lookup(RequestPath2); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - LookupContextPtr name_lookup_context3 = lookup(RequestPath3); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body1("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 - 1); - const std::string Body2("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2); - const std::string Body3("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 + 1); - - insert(move(name_lookup_context1), getResponseHeaders(), Body1); - insert(move(name_lookup_context2), getResponseHeaders(), Body2); - insert(move(name_lookup_context3), getResponseHeaders(), Body3); - - name_lookup_context1 = lookup(RequestPath1); - name_lookup_context2 = lookup(RequestPath2); - name_lookup_context3 = lookup(RequestPath3); - - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context2.get(), Body2)); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context3.get(), Body3)); - - clearMaps(); -} - -TEST_F(HazelcastDividedCacheTest, HandleRangedResponses) { - // make the size even for the sake of divisions below. - const int size = HazelcastTestUtil::TEST_PARTITION_SIZE & 1 ? - HazelcastTestUtil::TEST_PARTITION_SIZE + 1 : - HazelcastTestUtil::TEST_PARTITION_SIZE; - - const std::string RequestPath("/ranged/responses"); - - LookupContextPtr name_lookup_context1 = lookup(RequestPath); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body = std::string(size, 'h') + - std::string(size, 'z') + std::string(size, 'c'); - - insert(move(name_lookup_context1), getResponseHeaders(), Body); - name_lookup_context1 = lookup(RequestPath); - - EXPECT_EQ(std::string(size, 'h'), getBody(*name_lookup_context1, 0, size)); - EXPECT_EQ(std::string(size, 'z'), getBody(*name_lookup_context1, size, size * 2)); - EXPECT_EQ(std::string(size / 2, 'h') + std::string(size / 2, 'z'), - getBody(*name_lookup_context1, size / 2, size + size / 2)); - EXPECT_EQ(std::string("h") + std::string(size, 'z') + std::string("c"), - getBody(*name_lookup_context1, size - 1, 2 * size + 1)); - EXPECT_EQ(Body, getBody(*name_lookup_context1, 0, size * 3)); - clearMaps(); -} - TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { const std::string RequestPath("/abort/when/max/size/reached"); - InsertContextPtr inserter = hz_cache_->makeInsertContext(lookup(RequestPath)); - inserter->insertHeaders(getResponseHeaders(), false); + InsertContextPtr insert_context = makeInsertContext(RequestPath); + insert_context->insertHeaders(getResponseHeaders(), false); bool ready_for_next = true; while (ready_for_next) { - inserter->insertBody( + insert_context->insertBody( Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), [&](bool ready){ready_for_next = ready;}, false); } EXPECT_EQ( - (HazelcastTestUtil::TEST_MAX_BODY_SIZE / HazelcastTestUtil::TEST_PARTITION_SIZE) + - ((HazelcastTestUtil::TEST_MAX_BODY_SIZE % HazelcastTestUtil::TEST_PARTITION_SIZE) == 0 ? 0 : 1), - testBodyMap().size()); - - LookupContextPtr name_lookup_context = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody( - name_lookup_context.get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); - clearMaps(); + (HazelcastTestUtil::TEST_MAX_BODY_SIZE / HazelcastTestUtil::TEST_PARTITION_SIZE) + + ((HazelcastTestUtil::TEST_MAX_BODY_SIZE % HazelcastTestUtil::TEST_PARTITION_SIZE) == 0 ? + 0 : 1), testBodyMap().size()); + + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); } TEST_F(HazelcastDividedCacheTest, PreventOverridingCacheEntries) { const std::string RequestPath("/prevent/override/cached/response"); - LookupContextPtr name_lookup_context = lookup(RequestPath); + LookupContextPtr lookup_context = lookup(RequestPath); const std::string OriginalBody(HazelcastTestUtil::TEST_PARTITION_SIZE * 2, 'h'); - insert(move(name_lookup_context), getResponseHeaders(), OriginalBody); + insert(move(lookup_context), getResponseHeaders(), OriginalBody); - name_lookup_context = lookup(RequestPath); + lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); // A possible call to insertion below is filter's fault, not an expected behavior. const std::string OverriddenBody(HazelcastTestUtil::TEST_PARTITION_SIZE * 3, 'z'); - insert(move(name_lookup_context), getResponseHeaders(), OverriddenBody); + insert(move(lookup_context), getResponseHeaders(), OverriddenBody); - name_lookup_context = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), OriginalBody)); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), OriginalBody)); EXPECT_EQ(2, testBodyMap().size()); EXPECT_EQ(1, testHeaderMap().size()); - clearMaps(); } TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { const std::string RequestPath("/only/one/must/insert"); - LookupContextPtr name_lookup_context1 = lookup(RequestPath); + LookupContextPtr lookup_context1 = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // The first missed lookup must be allowed to make insertion. - ASSERT(!static_cast(*name_lookup_context1).isAborted()); + ASSERT(!static_cast(*lookup_context1).isAborted()); // Following ones must abort the insertion. - LookupContextPtr name_lookup_context2; + LookupContextPtr lookup_context2; std::thread t1([&] { // If the second lookup would not be performed in a separate thread, it will acquire // the lock even if it's already locked. This is because the key locks on Hazelcast // IMap are re-entrant. A locked key can be acquired by the same thread again and // again based on its pid. // TODO: Examine Envoy's threading model to ensure this case's safety. - name_lookup_context2 = lookup(RequestPath); + lookup_context2 = lookup(RequestPath); }); t1.join(); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - ASSERT(static_cast(*name_lookup_context2).isAborted()); + ASSERT(static_cast(*lookup_context2).isAborted()); const std::string Body("hazelcast"); // second context should not insert even if arrives before the first one. - insert(move(name_lookup_context2), getResponseHeaders(), Body); - name_lookup_context2 = lookup(RequestPath); + insert(move(lookup_context2), getResponseHeaders(), Body); + lookup_context2 = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // first one must do the insertion. - insert(move(name_lookup_context1), getResponseHeaders(), Body); - name_lookup_context1 = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body)); - clearMaps(); + insert(move(lookup_context1), getResponseHeaders(), Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body)); } TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { const std::string RequestPath1("/miss/on/version/mismatch"); - LookupContextPtr name_lookup_context = lookup(RequestPath1); + LookupContextPtr lookup_context = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); + static_cast(*lookup_context).variantHashKey(); const std::string Body(HazelcastTestUtil::TEST_PARTITION_SIZE * 2, 'h'); - insert(move(name_lookup_context), getResponseHeaders(), Body); - name_lookup_context = lookup(RequestPath1); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), Body)); + insert(move(lookup_context), getResponseHeaders(), Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath1).get(), Body)); // Change version of the second partition. const std::string body2key = getBodyKey(variant_hash_key,1); @@ -179,34 +113,33 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { testBodyMap().put(body2key, *body2); // Change happened in the second partition. Lookup to the first one should be successful. - name_lookup_context = lookup(RequestPath1); - std::string partition1 = getBody(*name_lookup_context, 0, HazelcastTestUtil::TEST_PARTITION_SIZE); + lookup_context = lookup(RequestPath1); + std::string partition1 = getBody(*lookup_context, 0, + HazelcastTestUtil::TEST_PARTITION_SIZE); EXPECT_EQ(partition1, std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')); - std::string fullBody = getBody(*name_lookup_context, 0, HazelcastTestUtil::TEST_PARTITION_SIZE * 2); + std::string fullBody = getBody(*lookup_context, 0, + HazelcastTestUtil::TEST_PARTITION_SIZE * 2); EXPECT_EQ(fullBody, HazelcastTestUtil::abortedBodyResponse()); // Clean up must be performed for malformed entries. EXPECT_EQ(0, testBodyMap().size()); EXPECT_EQ(0, testHeaderMap().size()); - - clearMaps(); } -TEST_F(HazelcastDividedCacheTest, MissLookupOnDifferentKey) { +TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { const std::string RequestPath("/miss/on/different/key"); - LookupContextPtr name_lookup_context = lookup(RequestPath); + LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); + static_cast(*lookup_context).variantHashKey(); const std::string Body("hazelcast"); - insert(move(name_lookup_context), getResponseHeaders(), Body); - name_lookup_context = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), Body)); + insert(move(lookup_context), getResponseHeaders(), Body); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body)); // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. @@ -217,55 +150,54 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnDifferentKey) { header->variantKey(std::move(modified)); testHeaderMap().put(mapKey(variant_hash_key), *header); - name_lookup_context = lookup(RequestPath); + lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // New entry insertion should be aborted and not override the existing one with the // same hash key. This scenario is possible if there is a hash collision. No eviction // or clean up is expected. Since overriding an entry is prevented. - insert(move(name_lookup_context), getResponseHeaders(), Body); - name_lookup_context = lookup(RequestPath); + insert(move(lookup_context), getResponseHeaders(), Body); + lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); EXPECT_EQ(1, testHeaderMap().size()); - clearMaps(); } TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { const std::string RequestPath1("/clean/up/on/missing/body"); - LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + LookupContextPtr lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*name_lookup_context1).variantHashKey(); + static_cast(*lookup_context1).variantHashKey(); const std::string Body = std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h') + - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z') + - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'); + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z') + + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'); - insert(move(name_lookup_context1), getResponseHeaders(), Body); - name_lookup_context1 = lookup(RequestPath1); + insert(move(lookup_context1), getResponseHeaders(), Body); + lookup_context1 = lookup(RequestPath1); // Response is cached with the following pattern: // variant_hash_key -> HeaderEntry (in header map) // variant_hash_key "0" -> Body1 (in body map) // variant_hash_key "1" -> Body2 (in body map) // variant_hash_key "2" -> Body3 (in body map) - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); removeBody(variant_hash_key, 1); // evict Body2. - name_lookup_context1 = lookup(RequestPath1); + lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); // Lookup for Body1 is OK. - name_lookup_context1->getBody({0, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, + lookup_context1->getBody({0, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, [](Buffer::InstancePtr&& data) {EXPECT_NE(data, nullptr);}); // Lookup for Body2 must fail and trigger clean up. - name_lookup_context1->getBody( + lookup_context1->getBody( {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, [](Buffer::InstancePtr&& data){EXPECT_EQ(data, nullptr);}); - name_lookup_context1 = lookup(RequestPath1); + lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // On lookup miss, lock is being acquired. It must be released @@ -273,7 +205,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { // If not released, the second run for the test fails. Since no // insertion follows the missed lookup here, the lock is explicitly // unlocked. - hz_cache_->unlock(variant_hash_key); + unlockKey(variant_hash_key); // Assert clean up EXPECT_EQ(0, testBodyMap().size()); @@ -282,10 +214,10 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { auto headerOnlyTest = [this](std::string path, bool empty_body) { - LookupContextPtr name_lookup_context = lookup(path); + LookupContextPtr lookup_context = lookup(path); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - insert(move(name_lookup_context), getResponseHeaders(), empty_body ? "" : nullptr); - name_lookup_context = lookup(path); + insert(move(lookup_context), getResponseHeaders(), empty_body ? "" : nullptr); + lookup_context = lookup(path); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); EXPECT_EQ(0, lookup_result_.content_length_); }; @@ -298,144 +230,52 @@ TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { headerOnlyTest("/empty/body/response", true); EXPECT_EQ(0, testBodyMap().size()); - clearMaps(); -} - -// Tests belong to SimpleHttpCache are applied below with minor changes on the test body. -TEST_F(HazelcastDividedCacheTest, SimplePutGet) { - const std::string RequestPath1("/simple/put/first"); - LookupContextPtr name_lookup_context1 = lookup(RequestPath1); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body1("hazelcast"); - insert(move(name_lookup_context1), getResponseHeaders(), Body1); - - name_lookup_context1 = lookup(RequestPath1); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); - - const std::string& RequestPath2("/simple/put/second"); - LookupContextPtr name_lookup_context2 = lookup(RequestPath2); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body2("hazelcast.distributed.caching"); - - insert(move(name_lookup_context2), getResponseHeaders(), Body2); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); - clearMaps(); -} - -TEST_F(HazelcastDividedCacheTest, PrivateResponse) { - const std::string request_path("/private/response"); - - LookupContextPtr name_lookup_context = lookup(request_path); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body("Value"); - // We must make sure at cache insertion time, private responses must not be - // inserted. However, if the insertion did happen, it would be served at the - // time of lookup. - insert(move(name_lookup_context), getResponseHeaders(), Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); - clearMaps(); } -TEST_F(HazelcastDividedCacheTest, Miss) { - LookupContextPtr name_lookup_context = lookup("/no/such/entry"); - uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - // Do not left over a missed lookup without inserting or releasing its lock. - hz_cache_->unlock(variant_hash_key); -} +TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { + { + const std::string RequestPath("/online/offline/then/online"); + LookupContextPtr lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); -TEST_F(HazelcastDividedCacheTest, Fresh) { - const Http::TestResponseHeaderMapImpl response_headers = { - {"date", formatter_.fromTime(current_time_)}, - {"cache-control", "public, max-age=3600"}}; - insert("/", response_headers, ""); - time_source_.sleep(std::chrono::seconds(3600)); - lookup("/"); - EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - clearMaps(); -} + const std::string Body("s", HazelcastTestUtil::TEST_PARTITION_SIZE); + insert(move(lookup_context), getResponseHeaders(), Body); + lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); -TEST_F(HazelcastDividedCacheTest, Stale) { - const Http::TestResponseHeaderMapImpl response_headers = { - {"date", formatter_.fromTime(current_time_)}, - {"cache-control", "public, max-age=3600"}}; - insert("/", response_headers, ""); - time_source_.sleep(std::chrono::seconds(3601)); - lookup("/"); - EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - clearMaps(); -} + dropConnection(); -TEST_F(HazelcastDividedCacheTest, RequestSmallMinFresh) { - request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "min-fresh=1000"); - const std::string request_path("/request/small/min/fresh"); - LookupContextPtr name_lookup_context = lookup(request_path); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + insert(move(lookup_context), getResponseHeaders(), Body); - Http::TestResponseHeaderMapImpl response_headers{ - {"date", formatter_.fromTime(current_time_)}, - {"age", "6000"}, - {"cache-control", "public, max-age=9000"}}; - const std::string Body("Value"); - insert(move(name_lookup_context), response_headers, Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); - clearMaps(); -} + restoreConnection(); -TEST_F(HazelcastDividedCacheTest, ResponseStaleWithRequestLargeMaxStale) { - request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "max-stale=9000"); + lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); + } - const std::string request_path("/response/stale/with/request/large/max/stale"); - LookupContextPtr name_lookup_context = lookup(request_path); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + { + const std::string RequestPath("/connection/lost/during/body/insert"); + InsertContextPtr insert_context = makeInsertContext(RequestPath); + insert_context->insertHeaders(getResponseHeaders(), false); + insert_context->insertBody(Buffer::OwnedImpl( + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), [](bool) {}, false); + insert_context->insertBody(Buffer::OwnedImpl( + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z')), [](bool) {}, false); - Http::TestResponseHeaderMapImpl response_headers{ - {"date", formatter_.fromTime(current_time_)}, - {"age", "7200"}, - {"cache-control", "public, max-age=3600"}}; + dropConnection(); - const std::string Body("Value"); - insert(move(name_lookup_context), response_headers, Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); - clearMaps(); -} + insert_context->insertBody(Buffer::OwnedImpl( + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c')), [](bool) {}, false); + LookupContextPtr lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); -TEST_F(HazelcastDividedCacheTest, StreamingPut) { - InsertContextPtr inserter = hz_cache_-> - makeInsertContext(lookup("/streaming/put")); - inserter->insertHeaders(getResponseHeaders(), false); - inserter->insertBody( - Buffer::OwnedImpl("Hello, "), - [](bool ready){EXPECT_TRUE(ready); }, false); - inserter->insertBody(Buffer::OwnedImpl("World!"), nullptr, true); - LookupContextPtr name_lookup_context = lookup("/streaming/put"); - EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - EXPECT_NE(nullptr, lookup_result_.headers_); - ASSERT_EQ(13, lookup_result_.content_length_); - EXPECT_EQ("Hello, World!", getBody(*name_lookup_context, 0, 13)); - EXPECT_EQ("o, World!", getBody(*name_lookup_context, 4, 13)); - clearMaps(); -} + restoreConnection(); -TEST(Registration, GetFactory) { - HttpCacheFactory* factory = Registry::FactoryRegistry::getFactoryByType( - "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); - ASSERT_NE(factory, nullptr); - envoy::extensions::filters::http::cache::v3alpha::CacheConfig config; - HazelcastHttpCacheConfig hz_cache_config = HazelcastTestUtil::getTestConfig(false); - config.mutable_typed_config()->PackFrom(hz_cache_config); - HazelcastHttpCache& cache = static_cast(factory->getCache(config)); - EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); - - // Explicitly destroy Hazelcast connection here. Otherwise the test - // environment does not wait for cache destructor to be completed - // and this causes segfault when Hazelcast Client is shutting down. - cache.shutdown(); + lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + } } } // namespace HazelcastHttpCache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index eb392614a28d7..09785769b21f7 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -11,158 +11,95 @@ namespace HttpFilters { namespace Cache { namespace HazelcastHttpCache { +/** + * Tests for UNIFIED cache mode. + */ class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { -protected: - static void SetUpTestSuite() { + void SetUp() { HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(true); - hz_cache_ = new HazelcastHttpCache(cfg); - hz_cache_->connect(); + cache_ = new HazelcastHttpCache(cfg); + cache_->connect(); clearMaps(); } }; -TEST_F(HazelcastUnifiedCacheTest, SimpleMissAndPutEntries) { - const std::string RequestPath1("/size/minus/one"); - const std::string RequestPath2("/exactly/size"); - const std::string RequestPath3("/size/plus/one"); - - LookupContextPtr name_lookup_context1 = lookup(RequestPath1); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - LookupContextPtr name_lookup_context2 = lookup(RequestPath2); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - LookupContextPtr name_lookup_context3 = lookup(RequestPath3); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body1("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 - 1); - const std::string Body2("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2); - const std::string Body3("s", HazelcastTestUtil::TEST_PARTITION_SIZE * 2 + 1); - - insert(move(name_lookup_context1), getResponseHeaders(), Body1); - insert(move(name_lookup_context2), getResponseHeaders(), Body2); - insert(move(name_lookup_context3), getResponseHeaders(), Body3); - - name_lookup_context1 = lookup(RequestPath1); - name_lookup_context2 = lookup(RequestPath2); - name_lookup_context3 = lookup(RequestPath3); - - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context2.get(), Body2)); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context3.get(), Body3)); - - clearMaps(); -} - -TEST_F(HazelcastUnifiedCacheTest, HandleRangedResponses) { - // make the size even for the sake of divisions below. - const int size = HazelcastTestUtil::TEST_PARTITION_SIZE & 1 ? - HazelcastTestUtil::TEST_PARTITION_SIZE + 1 : - HazelcastTestUtil::TEST_PARTITION_SIZE; - - const std::string RequestPath("/ranged/responses"); - - LookupContextPtr name_lookup_context1 = lookup(RequestPath); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body = std::string(size, 'h') + - std::string(size, 'z') + std::string(size, 'c'); - - insert(move(name_lookup_context1), getResponseHeaders(), Body); - name_lookup_context1 = lookup(RequestPath); - - EXPECT_EQ(std::string(size, 'h'), getBody(*name_lookup_context1, 0, size)); - EXPECT_EQ(std::string(size, 'z'), getBody(*name_lookup_context1, size, size * 2)); - EXPECT_EQ(std::string(size / 2, 'h') + std::string(size / 2, 'z'), - getBody(*name_lookup_context1, size / 2, size + size / 2)); - EXPECT_EQ(std::string("h") + std::string(size, 'z') + std::string("c"), - getBody(*name_lookup_context1, size - 1, 2 * size + 1)); - EXPECT_EQ(Body, getBody(*name_lookup_context1, 0, size * 3)); - clearMaps(); -} - TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedInsertionWhenMaxSizeReached) { const std::string RequestPath("/abort/when/max/size/reached"); - InsertContextPtr inserter = hz_cache_->makeInsertContext(lookup(RequestPath)); - inserter->insertHeaders(getResponseHeaders(), false); + InsertContextPtr insert_context = makeInsertContext(RequestPath); + insert_context->insertHeaders(getResponseHeaders(), false); bool ready_for_next = true; while (ready_for_next) { - inserter->insertBody( + insert_context->insertBody( Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE / 3, 'h')), [&](bool ready){ready_for_next = ready;}, false); } - LookupContextPtr name_lookup_context = lookup(RequestPath); EXPECT_TRUE(expectLookupSuccessWithBody( - name_lookup_context.get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); - clearMaps(); + lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); } -TEST_F(HazelcastUnifiedCacheTest, PutResponseIfAbsent) { +TEST_F(HazelcastUnifiedCacheTest, PutResponseOnlyWhenAbsent) { const std::string RequestPath("/only/one/must/insert"); - LookupContextPtr name_lookup_context1 = lookup(RequestPath); + LookupContextPtr lookup_context1 = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - LookupContextPtr name_lookup_context2 = lookup(RequestPath); + LookupContextPtr lookup_context2 = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); const std::string Body1("hazelcast"); const std::string Body2("hazelcast.distributed.caching"); // The second context should insert if the cache is empty for this request. - insert(move(name_lookup_context1), getResponseHeaders(), Body1); - name_lookup_context1 = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); + insert(move(lookup_context1), getResponseHeaders(), Body1); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body1)); // The first context should not do the insertion/override the existing value. - insert(move(name_lookup_context2), getResponseHeaders(), Body2); + insert(move(lookup_context2), getResponseHeaders(), Body2); // Response body must remain as Body1 - name_lookup_context2 = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context2.get(), Body1)); - clearMaps(); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body1)); } TEST_F(HazelcastUnifiedCacheTest, DoNotOverrideExistingResponse) { const std::string RequestPath1("/on/unified/not/override"); - LookupContextPtr name_lookup_context1 = lookup(RequestPath1); + LookupContextPtr lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - LookupContextPtr name_lookup_context2 = lookup(RequestPath1); + LookupContextPtr lookup_context2 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); const std::string Body1("hazelcast-first"); const std::string Body2("hazelcast-second"); - insert(move(name_lookup_context1), getResponseHeaders(), Body1); - insert(move(name_lookup_context2), getResponseHeaders(), Body2); + insert(move(lookup_context1), getResponseHeaders(), Body1); + insert(move(lookup_context2), getResponseHeaders(), Body2); - name_lookup_context1 = lookup(RequestPath1); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); - clearMaps(); + lookup_context1 = lookup(RequestPath1); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body1)); } -TEST_F(HazelcastUnifiedCacheTest, HeaderOnlyResponse) { - InsertContextPtr inserter = hz_cache_->makeInsertContext(lookup("/header/only")); - inserter->insertHeaders(getResponseHeaders(), true); - LookupContextPtr name_lookup_context = lookup("/header/only"); +TEST_F(HazelcastUnifiedCacheTest, UnifiedHeaderOnlyResponse) { + InsertContextPtr insert_context = makeInsertContext("/header/only"); + insert_context->insertHeaders(getResponseHeaders(), true); + LookupContextPtr lookup_context = lookup("/header/only"); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); EXPECT_EQ(0, lookup_result_.content_length_); - clearMaps(); } -TEST_F(HazelcastUnifiedCacheTest, MissLookupOnDifferentKey) { +TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { const std::string RequestPath("/miss/on/different/key"); - LookupContextPtr name_lookup_context = lookup(RequestPath); + LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); + static_cast(*lookup_context).variantHashKey(); const std::string Body("hazelcast"); - insert(move(name_lookup_context), getResponseHeaders(), Body); - name_lookup_context = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context.get(), Body)); + insert(move(lookup_context), getResponseHeaders(), Body); + lookup_context = lookup(RequestPath); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. @@ -173,154 +110,38 @@ TEST_F(HazelcastUnifiedCacheTest, MissLookupOnDifferentKey) { response->header().variantKey(std::move(modified)); testResponseMap().put(mapKey(variant_hash_key), *response); - name_lookup_context = lookup(RequestPath); + lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // New entry insertion should be aborted and not override the existing one with the // same hash key. This scenario is possible if there is a hash collision. No eviction // or clean up is expected. Since overriding an entry is prevented. - insert(move(name_lookup_context), getResponseHeaders(), Body); - name_lookup_context = lookup(RequestPath); + insert(move(lookup_context), getResponseHeaders(), Body); + lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); EXPECT_EQ(1, testResponseMap().size()); - clearMaps(); -} - -// Tests belong to SimpleHttpCache are applied below with minor changes on the test body. -TEST_F(HazelcastUnifiedCacheTest, SimplePutGet) { - const std::string RequestPath1("/simple/put/first"); - LookupContextPtr name_lookup_context1 = lookup(RequestPath1); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body1("hazelcast"); - insert(move(name_lookup_context1), getResponseHeaders(), Body1); - - name_lookup_context1 = lookup(RequestPath1); - EXPECT_TRUE(expectLookupSuccessWithBody(name_lookup_context1.get(), Body1)); - - const std::string& RequestPath2("/simple/put/second"); - LookupContextPtr name_lookup_context2 = lookup(RequestPath2); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body2("hazelcast.distributed.caching"); - - insert(move(name_lookup_context2), getResponseHeaders(), Body2); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); - clearMaps(); -} - -TEST_F(HazelcastUnifiedCacheTest, PrivateResponse) { - const std::string request_path("/private/response"); - - LookupContextPtr name_lookup_context = lookup(request_path); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - const std::string Body("Value"); - // We must make sure at cache insertion time, private responses must not be - // inserted. However, if the insertion did happen, it would be served at the - // time of lookup. - insert(move(name_lookup_context), getResponseHeaders(), Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); - clearMaps(); -} - -TEST_F(HazelcastUnifiedCacheTest, Miss) { - LookupContextPtr name_lookup_context = lookup("/no/such/entry"); - uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - - // Do not left over a missed lookup without inserting or releasing its lock. - hz_cache_->unlock(variant_hash_key); -} - -TEST_F(HazelcastUnifiedCacheTest, Fresh) { - const Http::TestResponseHeaderMapImpl response_headers = { - {"date", formatter_.fromTime(current_time_)}, - {"cache-control", "public, max-age=3600"}}; - insert("/", response_headers, ""); - time_source_.sleep(std::chrono::seconds(3600)); - lookup("/"); - EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - clearMaps(); -} - -TEST_F(HazelcastUnifiedCacheTest, Stale) { - const Http::TestResponseHeaderMapImpl response_headers = { - {"date", formatter_.fromTime(current_time_)}, - {"cache-control", "public, max-age=3600"}}; - insert("/", response_headers, ""); - time_source_.sleep(std::chrono::seconds(3601)); - lookup("/"); - EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - clearMaps(); } -TEST_F(HazelcastUnifiedCacheTest, RequestSmallMinFresh) { - request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "min-fresh=1000"); - const std::string request_path("/request/small/min/fresh"); - LookupContextPtr name_lookup_context = lookup(request_path); +TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { + const std::string RequestPath1("/online/"); + LookupContextPtr lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - Http::TestResponseHeaderMapImpl response_headers{ - {"date", formatter_.fromTime(current_time_)}, - {"age", "6000"}, - {"cache-control", "public, max-age=9000"}}; - const std::string Body("Value"); - insert(move(name_lookup_context), response_headers, Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); - clearMaps(); -} + const std::string Body("s", HazelcastTestUtil::TEST_PARTITION_SIZE); + insert(move(lookup_context1), getResponseHeaders(), Body); + lookup_context1 = lookup(RequestPath1); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); -TEST_F(HazelcastUnifiedCacheTest, ResponseStaleWithRequestLargeMaxStale) { - request_headers_.setReferenceKey(Http::Headers::get().CacheControl, "max-stale=9000"); + dropConnection(); - const std::string request_path("/response/stale/with/request/large/max/stale"); - LookupContextPtr name_lookup_context = lookup(request_path); + lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + insert(move(lookup_context1), getResponseHeaders(), Body); - Http::TestResponseHeaderMapImpl response_headers{ - {"date", formatter_.fromTime(current_time_)}, - {"age", "7200"}, - {"cache-control", "public, max-age=3600"}}; - - const std::string Body("Value"); - insert(move(name_lookup_context), response_headers, Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); - clearMaps(); -} - -TEST_F(HazelcastUnifiedCacheTest, StreamingPut) { - InsertContextPtr inserter = hz_cache_-> - makeInsertContext(lookup("/streaming/put")); - inserter->insertHeaders(getResponseHeaders(), false); - inserter->insertBody( - Buffer::OwnedImpl("Hello, "), - [](bool ready){EXPECT_TRUE(ready); }, false); - inserter->insertBody(Buffer::OwnedImpl("World!"), nullptr, true); - LookupContextPtr name_lookup_context = lookup("/streaming/put"); - EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - EXPECT_NE(nullptr, lookup_result_.headers_); - ASSERT_EQ(13, lookup_result_.content_length_); - EXPECT_EQ("Hello, World!", getBody(*name_lookup_context, 0, 13)); - EXPECT_EQ("o, World!", getBody(*name_lookup_context, 4, 13)); - clearMaps(); -} + restoreConnection(); -TEST(Registration, GetFactory) { - HttpCacheFactory* factory = Registry::FactoryRegistry::getFactoryByType( - "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); - ASSERT_NE(factory, nullptr); - envoy::extensions::filters::http::cache::v3alpha::CacheConfig config; - HazelcastHttpCacheConfig hz_cache_config = HazelcastTestUtil::getTestConfig(true); - config.mutable_typed_config()->PackFrom(hz_cache_config); - HazelcastHttpCache& cache = static_cast(factory->getCache(config)); - EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); - - // Explicitly destroy Hazelcast connection here. Otherwise the test - // environment does not wait for cache destructor to be completed - // and this causes segfault when Hazelcast Client is shutting down. - cache.shutdown(); + lookup_context1 = lookup(RequestPath1); + EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); } } // namespace HazelcastHttpCache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h index dc12acc77dc0b..1ff6f7a6b36ce 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -1,5 +1,9 @@ +#pragma once + #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" #include "gtest/gtest.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" namespace Envoy { namespace Extensions { @@ -13,8 +17,12 @@ class HazelcastTestUtil { static constexpr int TEST_PARTITION_SIZE = 10; static constexpr int TEST_MAX_BODY_SIZE = TEST_PARTITION_SIZE * 20; - static const std::string& abortedBodyResponse() - { + static Runtime::RandomGeneratorImpl& randomGenerator(){ + static Runtime::RandomGeneratorImpl rand; + return rand; + } + + static const std::string& abortedBodyResponse(){ static std::string response("NULL_BODY"); return response; } @@ -26,8 +34,11 @@ class HazelcastTestUtil { HazelcastHttpCacheConfig::MemberAddress* memberAddress = hc.add_addresses(); memberAddress->set_ip("127.0.0.1"); memberAddress->set_port(5701); + hc.set_invocation_timeout(1); hc.set_body_partition_size(TEST_PARTITION_SIZE); - hc.set_app_prefix("test"); + // During parallel tests, if caches do not have different prefixes, the entries + // and hence the results will be different than the expected. + hc.set_app_prefix(randomGenerator().uuid()); hc.set_unified(unified); hc.set_max_body_size(TEST_MAX_BODY_SIZE); return hc; @@ -42,8 +53,13 @@ class HazelcastTestUtil { }; -// TODO: If running a Hazelcast server is not possible during tests, -// this base will serve mock cache for testing. +/** + * The base environment for DIVIDED and UNIFIED cache mode tests. + * + * A similar test environment to SimpleHttpCacheTest is applied and + * some functions & fields are derived directly. + * + */ class HazelcastHttpCacheTestBase : public testing::Test { protected: @@ -51,37 +67,9 @@ class HazelcastHttpCacheTestBase : public testing::Test { HazelcastTestUtil::setRequestHeaders(request_headers_); } - static void TearDownTestSuite() { - delete hz_cache_; - hz_cache_ = nullptr; - } - - // Performs a cache lookup. - LookupContextPtr lookup(absl::string_view request_path) { - LookupRequest request = makeLookupRequest(request_path); - LookupContextPtr context = hz_cache_->makeLookupContext(std::move(request)); - context->getHeaders([this](LookupResult&& result) {lookup_result_ = std::move(result); }); - return context; - } - - // Inserts a value into the cache. - void insert(LookupContextPtr lookup, const Http::TestResponseHeaderMapImpl& response_headers, - const absl::string_view response_body) { - InsertContextPtr inserter = hz_cache_->makeInsertContext(move(lookup)); - inserter->insertHeaders(response_headers, response_body == nullptr); - if (response_body == nullptr) return; - inserter->insertBody(Buffer::OwnedImpl(response_body), nullptr, true); - } - - void insert(absl::string_view request_path, - const Http::TestResponseHeaderMapImpl& response_headers, - const absl::string_view response_body) { - insert(lookup(request_path), response_headers, response_body); - } - // Makes getBody requests until requested range is satisfied. - // Returns the bod on success, HazelcastTestUtil::abortedBodyResponse() on - // abortion by cache itself. + // Returns the body on success; HazelcastTestUtil::abortedBodyResponse() on + // abortion by cache. std::string getBody(LookupContext& context, uint64_t start, uint64_t end) { std::string full_body, body_chunk; uint64_t offset = start; @@ -105,6 +93,90 @@ class HazelcastHttpCacheTestBase : public testing::Test { return full_body; } + Http::TestResponseHeaderMapImpl getResponseHeaders() { + return Http::TestResponseHeaderMapImpl{ + {"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public, max-age=3600"}}; + } + + /// Test environments make cache calls over these functions. + + void unlockKey(uint64_t key) { + cache_->unlock(key); + } + + InsertContextPtr makeInsertContext(absl::string_view path) { + return cache_->makeInsertContext(lookup(path)); + } + + void clearMaps() { + if (cache_->unified_) { + cache_->getResponseMap().clear(); + } else { + cache_->getBodyMap().clear(); + cache_->getHeaderMap().clear(); + } + } + + void removeBody(uint64_t key, uint64_t order) { + ASSERT(!cache_->unified_); + cache_->getBodyMap().remove(cache_->orderedMapKey(key, order)); + } + + IMap testHeaderMap() { + ASSERT(!cache_->unified_); + return cache_->getHeaderMap(); + }; + + IMap testBodyMap() { + ASSERT(!cache_->unified_); + return cache_->getBodyMap(); + }; + + IMap testResponseMap() { + ASSERT(cache_->unified_); + return cache_->getResponseMap(); + }; + + std::string getBodyKey(uint64_t key, uint64_t order){ + return cache_->orderedMapKey(key, order); + } + + int64_t mapKey(uint64_t key){ + return cache_->mapKey(key); + } + + void dropConnection(){ + cache_->shutdown(false); + } + + void restoreConnection() { + cache_->connect(); + } + + /// from SimpleHttpCacheTest + + LookupContextPtr lookup(absl::string_view request_path) { + LookupRequest request = makeLookupRequest(request_path); + LookupContextPtr context = cache_->makeLookupContext(std::move(request)); + context->getHeaders([this](LookupResult&& result) {lookup_result_ = std::move(result); }); + return context; + } + + void insert(LookupContextPtr lookup, const Http::TestResponseHeaderMapImpl& response_headers, + const absl::string_view response_body) { + InsertContextPtr insert_context = cache_->makeInsertContext(move(lookup)); + insert_context->insertHeaders(response_headers, response_body == nullptr); + if (response_body == nullptr) return; + insert_context->insertBody(Buffer::OwnedImpl(response_body), nullptr, true); + } + + void insert(absl::string_view request_path, + const Http::TestResponseHeaderMapImpl& response_headers, + const absl::string_view response_body) { + insert(lookup(request_path), response_headers, response_body); + } + LookupRequest makeLookupRequest(absl::string_view request_path) { request_headers_.setPath(request_path); return LookupRequest(request_headers_, current_time_); @@ -112,6 +184,7 @@ class HazelcastHttpCacheTestBase : public testing::Test { AssertionResult expectLookupSuccessWithBody(LookupContext* lookup_context, absl::string_view body) { + // From SimpleHttpCacheTest if (lookup_result_.cache_entry_status_ != CacheEntryStatus::Ok) { return AssertionFailure() << "Expected: lookup_result_.cache_entry_status" " == CacheEntryStatus::Ok\n Actual: " @@ -125,74 +198,19 @@ class HazelcastHttpCacheTestBase : public testing::Test { } const std::string actual_body = getBody(*lookup_context, 0, body.size()); if (body != actual_body) { - return AssertionFailure() << "Expected body == " << body << - "\n Actual: " << actual_body; + return AssertionFailure() << "Expected body == " << body << "\n Actual: " << actual_body; } return AssertionSuccess(); } - Http::TestResponseHeaderMapImpl getResponseHeaders() { - return Http::TestResponseHeaderMapImpl{ - {"date", formatter_.fromTime(current_time_)}, - {"cache-control", "public,max-age=3600"}}; - } - - static HazelcastHttpCache* hz_cache_; + HazelcastHttpCache* cache_; LookupResult lookup_result_; Http::TestRequestHeaderMapImpl request_headers_; Event::SimulatedTimeSystem time_source_; SystemTime current_time_ = time_source_.systemTime(); DateFormatter formatter_{"%a, %d %b %Y %H:%M:%S GMT"}; - - // Helpers for test environment. - static void clearMaps() { - if (hz_cache_->unified_) { - hz_cache_->getResponseMap().clear(); - } else { - hz_cache_->getBodyMap().clear(); - hz_cache_->getHeaderMap().clear(); - } - } - - void removeBody(uint64_t key, uint64_t order) { - ASSERT(!hz_cache_->unified_); - hz_cache_->getBodyMap().remove(hz_cache_->orderedMapKey(key, order)); - } - - IMap testHeaderMap() { - ASSERT(!hz_cache_->unified_); - return hz_cache_->getHeaderMap(); - }; - - IMap testBodyMap() { - ASSERT(!hz_cache_->unified_); - return hz_cache_->getBodyMap(); - }; - - IMap testResponseMap() { - ASSERT(hz_cache_->unified_); - return hz_cache_->getResponseMap(); - }; - - bool isUnified() { - return hz_cache_->unified_; - } - - std::string getBodyKey(uint64_t key, uint64_t order){ - return hz_cache_->orderedMapKey(key, order); - } - - int64_t mapKey(uint64_t key){ - return hz_cache_->mapKey(key); - } }; -// Since creating the cache (connecting to the cluster), -// is not light weight, use a single cache through the test environment. -// TODO: Check for parallel tests. Using a random app_prefix per test might solve -// the concurrent request issues. However, destroying cache is still a problem. -HazelcastHttpCache* HazelcastHttpCacheTestBase::hz_cache_ = nullptr; - } // namespace HazelcastHttpCache } // namespace Cache } // namespace HttpFilters From 8630701ea1f74c6a14ef1872eface1f106ef8f3f Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Fri, 3 Apr 2020 16:19:26 +0300 Subject: [PATCH 05/33] Add local cache to use during tests. Signed-off-by: Enes Ozcan --- bazel/foreign_cc/BUILD | 1 - .../http/cache/hazelcast_http_cache/BUILD | 27 ++- .../cache/hazelcast_http_cache/config.proto | 112 +++++----- .../hazelcast_http_cache/hazelcast_cache.h | 198 ++++++++++++++++++ .../hazelcast_cache_entry.cc | 39 ++-- .../hazelcast_cache_entry.h | 57 +++-- .../hazelcast_http_cache/hazelcast_context.cc | 104 +++++---- .../hazelcast_http_cache/hazelcast_context.h | 47 ++--- .../hazelcast_http_cache.h | 145 ------------- ..._cache.cc => hazelcast_http_cache_impl.cc} | 184 ++++++++-------- .../hazelcast_http_cache_impl.h | 121 +++++++++++ .../{config_util.h => util.h} | 55 ++--- .../http/cache/hazelcast_http_cache/BUILD | 15 +- .../hazelcast_common_cache_test.cc | 65 +++--- .../hazelcast_divided_cache_test.cc | 109 +++++----- .../hazelcast_test_cache.cc | 187 +++++++++++++++++ .../hazelcast_test_cache.h | 140 +++++++++++++ .../hazelcast_unified_cache_test.cc | 38 ++-- .../http/cache/hazelcast_http_cache/util.h | 117 +++-------- 19 files changed, 1125 insertions(+), 636 deletions(-) create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h delete mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h rename source/extensions/filters/http/cache/hazelcast_http_cache/{hazelcast_http_cache.cc => hazelcast_http_cache_impl.cc} (72%) create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h rename source/extensions/filters/http/cache/hazelcast_http_cache/{config_util.h => util.h} (55%) create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h diff --git a/bazel/foreign_cc/BUILD b/bazel/foreign_cc/BUILD index 5f7e7dc355725..119ed5b863eb9 100644 --- a/bazel/foreign_cc/BUILD +++ b/bazel/foreign_cc/BUILD @@ -181,7 +181,6 @@ envoy_cmake_external( cache_entries = { "HZ_LIB_TYPE": "STATIC", "CMAKE_BUILD_TYPE": "RELEASE", - "CMAKE_CXX_FLAGS" : "-Wno-deprecated", }, lib_source = "@com_github_hazelcast_cpp_client//:all", static_libraries = select({ diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 27aa7ab936267..be9bf17b15da0 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -3,31 +3,36 @@ licenses(["notice"]) # Apache 2 load( "//bazel:envoy_build_system.bzl", "envoy_cc_extension", - "envoy_proto_library", "envoy_package", + "envoy_proto_library", ) envoy_package() envoy_cc_extension( name = "hazelcast_http_cache_lib", - srcs = ["hazelcast_http_cache.cc", - "hazelcast_cache_entry.cc", - "hazelcast_context.cc"], - hdrs = ["hazelcast_http_cache.h", - "hazelcast_cache_entry.h", - "hazelcast_context.h", - "config_util.h"], - status = "wip", - security_posture = "robust_to_untrusted_downstream_and_upstream", + srcs = [ + "hazelcast_cache_entry.cc", + "hazelcast_context.cc", + "hazelcast_http_cache_impl.cc", + ], + hdrs = [ + "hazelcast_cache.h", + "hazelcast_cache_entry.h", + "hazelcast_context.h", + "hazelcast_http_cache_impl.h", + "util.h", + ], external_deps = ["hazelcast_cpp_client"], + security_posture = "robust_to_untrusted_downstream_and_upstream", + status = "wip", deps = [ ":config_cc_proto", + "//include/envoy/registry", "//source/common/buffer:buffer_lib", "//source/common/common:logger_lib", "//source/common/http:header_map_lib", "//source/common/runtime:runtime_lib", - "//include/envoy/registry", "//source/extensions/filters/http/cache:http_cache_lib", ], ) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto index d74d2efcd537e..31f91c0ad9557 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -9,70 +9,70 @@ package envoy.source.extensions.filters.http.cache; // Hazelcast Http Cache configuration message HazelcastHttpCacheConfig { - // Address for Hazelcast cluster member. - message MemberAddress { - string ip = 1; - int32 port = 2; - } + // Address for Hazelcast cluster member. + message MemberAddress { + string ip = 1; + int32 port = 2; + } - // Group name of Hazelcast cluster to be connected. Not only the address of a member - // but its group name must match. - string group_name = 1; - // Group password of Hazelcast cluster to be connected. - string group_password = 2; + // Group name of Hazelcast cluster to be connected. Not only the address of a member + // but its group name must match. + string group_name = 1; + // Group password of Hazelcast cluster to be connected. + string group_password = 2; - // The timeout value in milliseconds for Hazelcast members to accept this client's - // connection requests. If the member does not respond within the timeout, the client - // will think the connection is lost and retry to connect to cluster as many as - // connection_attempt_limit. 5000 by default and 0 is not allowed. - uint32 connection_timeout = 3; + // The timeout value in milliseconds for Hazelcast members to accept this client's + // connection requests. If the member does not respond within the timeout, the client + // will think the connection is lost and retry to connect to cluster as many as + // connection_attempt_limit. 5000 by default and 0 is not allowed. + uint32 connection_timeout = 3; - // When the client connection to the cluster is down, client will retry as many - // as connection_attempt_limit before giving up. After this much of retries, the - // client will go offline and cache will not be active from then on permanently. - // 10 by default and 0 is not allowed. - uint32 connection_attempt_limit = 4; + // When the client connection to the cluster is down, client will retry as many + // as connection_attempt_limit before giving up. After this much of retries, the + // client will go offline and cache will not be active from then on permanently. + // 10 by default and 0 is not allowed. + uint32 connection_attempt_limit = 4; - // The duration in milliseconds between the connection attempts to cluster. - // 5000 by default and 0 is not allowed. - uint32 connection_attempt_period = 5; + // The duration in milliseconds between the connection attempts to cluster. + // 5000 by default and 0 is not allowed. + uint32 connection_attempt_period = 5; - // The timeout value in seconds for a call to be responded by Hazelcast cluster. - // If a member does not respond within the timeout, the lookup or insert operation - // will be cancelled and treated as a cache miss or an aborted insertion. - // 8 by default and 0 is not allowed. - uint32 invocation_timeout = 6; + // The timeout value in seconds for a call to be responded by Hazelcast cluster. + // If a member does not respond within the timeout, the lookup or insert operation + // will be cancelled and treated as a cache miss or an aborted insertion. + // 8 by default and 0 is not allowed. + uint32 invocation_timeout = 6; - // Only one member address is enough to connect to the cluster but - // providing more than one is recommended. - // By default, 127.0.0.1:5701 will be tried. - repeated MemberAddress addresses = 7; + // Only one member address is enough to connect to the cluster but + // providing more than one is recommended. + // By default, 127.0.0.1:5701 will be tried. + repeated MemberAddress addresses = 7; - // Application specific name for the cache. Different deployments should - // use the same prefix if they want to share the same cache and connect - // to the same Hazelcast cluster. Empty string by default. - string app_prefix = 8; + // Application specific name for the cache. Different deployments should + // use the same prefix if they want to share the same cache and connect + // to the same Hazelcast cluster. Empty string by default. + string app_prefix = 8; - // In unified mode, cached responses will be stored as a single entry. - // On a range HTTP request, regardless of the request range, all the - // body will be called from the remote cache and then the requested - // range will be served. - // In divided mode, cached responses will be stored in two different - // maps: header map and body map. For a response to be cached, its - // header is stored in the header map and its body is stored in body - // map partitioned with body_partition_size. On a range request, only - // required body partitions are called from the distributed map. This - // option causes extra memory usage per partition on the cache. - // False by default. - bool unified = 9; + // In unified mode, cached responses will be stored as a single entry. + // On a range HTTP request, regardless of the request range, all the + // body will be called from the remote cache and then the requested + // range will be served. + // In divided mode, cached responses will be stored in two different + // maps: header map and body map. For a response to be cached, its + // header is stored in the header map and its body is stored in body + // map partitioned with body_partition_size. On a range request, only + // required body partitions are called from the distributed map. This + // option causes extra memory usage per partition on the cache. + // False by default. + bool unified = 9; - // Body partition size for divided cache. Ignored in unified mode. - // At most 64 KB allowed. 2 KB by default. - uint64 body_partition_size = 10; + // Body partition size for divided cache. Ignored in unified mode. + // At most 64 KB allowed. 2 KB by default. + uint64 body_partition_size = 10; - // Maximum allowed body size per response. If insertion for a larger - // value than the limit is attempted, the first max_body_size bytes - // of the response will be cached and the remaining will be ignored. - // At most 64 KB for UNIFIED mode, 2 MB for DIVIDED mode. - uint64 max_body_size = 11; + // Maximum allowed body size per response. If insertion for a larger + // value than the limit is attempted, the first max_body_size bytes + // of the response will be cached and the remaining will be ignored. + // At most 64 KB for UNIFIED mode, 2 MB for DIVIDED mode. + uint64 max_body_size = 11; } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h new file mode 100644 index 0000000000000..8d574ecc7392c --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h @@ -0,0 +1,198 @@ +#pragma once + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" +#include "extensions/filters/http/cache/http_cache.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +/** + * Cache abstraction to support DIVIDED and UNIFIED cache modes. + * + * In UNIFIED mode, an HTTP response is wrapped by a HazelcastResponseEntry + * with its all fields (headers, body, trailers, request key) and stored in + * distributed map. On a range Http request, regardless of the requested + * range, the whole response body is fetched from the cache. + * + * In DIVIDED mode, an HTTP response's fields except for its body are wrapped + * by a HazelcastHeaderEntry. It's body is divided into chunks with a certain + * size and then stored in another distributed map as HazelcastBodyEntry. On + * a range request, not the whole body for the response but only the necessary + * partitions are fetched from the cache. A header and its bodies have a common + * number named to interrelate multiple entries. + * + * @note Lookup and Insert contexts should use this base, not HttpCache. + */ +class HazelcastCache : public HttpCache { +public: + HazelcastCache(bool unified, uint64_t partition_size, uint64_t max_body_size) + : unified_(unified), body_partition_size_(partition_size), max_body_size_(max_body_size) {} + + /// Divided mode + + /** + * Puts a header entry into header cache. + * @param key Hash key for the entry + * @param entry Entry to be inserted + * @note Generated keys should be consistent across restarts, architectures, + * builds, and configurations. Otherwise, different filters using the + * same Hazelcast cluster might store the same response with different + * keys. + */ + virtual void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) PURE; + + /** + * Puts a body entry into body cache. + * @note The key for a body partition must be obtainable from its header key. + * @param key Hash key for the whole body + * @param order Order of the body chunk among other partitions + * @param entry Entry to be inserted + */ + virtual void putBody(const uint64_t& key, const uint64_t& order, + const HazelcastBodyEntry& entry) PURE; + + /** + * Performs a lookup to header cache for the given key. + * @param key Hash key for the entry + * @return HazelcastHeaderPtr to cached entry if found, nullptr otherwise + */ + virtual HazelcastHeaderPtr getHeader(const uint64_t& key) PURE; + + /** + * Performs a lookup to body cache for the given key and order pair. + * @param key Hash key for the whole body + * @param order Order of the body chunk among other partitions + * @return HazelcastBodyPtr to cached entry if found, nullptr otherwise. + */ + virtual HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order) PURE; + + /** + * Cleans up a malformed response when at least one of the body chunks are missed + * during lookup. All bodies and the header for the response are removed to make + * a new insertion available by an insert context. + * @param key Header key for the response + * @param version Version for the key and body + * @param body_size Total body size for the response + */ + virtual void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) PURE; + + /** + * Cleans up a malformed response when a body partition with different version + * than the header is encountered during lookup. + * @param key Header key for the response + * @param version Version for the key and body + * @param body_size Total body size for the response + */ + virtual void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) PURE; + + /// Unified mode + + /** + * Puts a unified entry into unified cache if no other entry associated with the key + * is found. + * @note IfAbsent is to prevent race between multiple filters. Overriding + * an existing entry is forbidden. HttpCache::updateHeaders() might + * be used when necessary. + * @param key Hash key for the entry + * @param entry Entry to be inserted + */ + virtual void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry) PURE; + + /** + * Performs a lookup to unified cache for the given key. + * @param key Hash key for the entry. + * @return HazelcastResponsePtr to cached entry if found, nullptr otherwise. + */ + virtual HazelcastResponsePtr getResponse(const uint64_t& key) PURE; + + /// Common + + /** + * Attempts to lock the given key in the cache. When a key is locked, a lookup + * can be performed but an insertion or update for the key must be prevented. + * @note Used to prevent multiple insertions or updates from different + * filters at a time. + * @param key Key to be locked. + * @return True if acquired, false otherwise. + */ + virtual bool tryLock(const uint64_t& key) PURE; + + /** + * Releases the lock for the key. + * @param Key to be unlocked + */ + virtual void unlock(const uint64_t& key) PURE; + + /** + * Produces a random number which is not necessarily perfect uniform or random. + * @return Random unsigned long. + * @note Primary use cases for the random number is to generate version + * for header and body entries. + */ + virtual uint64_t random() PURE; + + /** + * @note Ignored in UNIFIED mode. + * @return Size in bytes for a single body entry configured for the cache + */ + const uint64_t& bodySizePerEntry() { return body_partition_size_; }; + + /** + * @return Allowed max size in bytes for a response configured for the cache + * @note Common for both modes. For a response which has a body larger + * than this limit, the first max_body_size_ bytes of the response + * will be cached only. + */ + const uint64_t& maxBodySize() { return max_body_size_; }; + + /** + * Generates a unique signed key for an unsigned one. + * @param unsigned_key Unsigned hash key + * @return Signed unique key + * @note Hazelcast client accepts signed keys only. + */ + inline int64_t mapKey(const uint64_t& unsigned_key) { + // The reason for not static casting directly is a possible overflow + // for int64 on intermediate step for -2^63. + int64_t signed_key; + std::memcpy(&signed_key, &unsigned_key, sizeof(int64_t)); + return signed_key; + } + + /** + * Creates string keys for body partition entries obtainable from their header + * keys. + * @param key Unsigned hash key for the header + * @param order Order of the body among other partitions starting from 0 + * @return Body partition key unique for header and order pair + * + * @note Appending '#' or any other marker between the key and order + * string is required. Otherwise, for instance, the 11th order + * body for key 1 and the 1st order body for key 11 will have + * the same map key "111". + */ + inline std::string orderedMapKey(const uint64_t& key, const uint64_t& order) { + return std::to_string(key).append("#").append(std::to_string(order)); + } + + virtual ~HazelcastCache() = default; + +protected: + /** Cache mode */ + const bool unified_; + + /** Partition size in bytes for a single body entry */ + const uint64_t body_partition_size_; + + /** Allowed max size in bytes for a response */ + const uint64_t max_body_size_; +}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc index c21101104d71a..5cb1ee0c4fb87 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc @@ -63,8 +63,9 @@ void HazelcastHeaderEntry::readUnifiedData(ObjectDataInput& reader) { HazelcastHeaderEntry::HazelcastHeaderEntry() = default; HazelcastHeaderEntry::HazelcastHeaderEntry(Http::ResponseHeaderMapPtr&& header_map, Key&& key, - uint64_t body_size, int32_t version) : header_map_(std::move(header_map)), - variant_key_(std::move(key)), body_size_(body_size), version_(version) {} + uint64_t body_size, int32_t version) + : header_map_(std::move(header_map)), variant_key_(std::move(key)), body_size_(body_size), + version_(version) {} HazelcastHeaderEntry::HazelcastHeaderEntry(const HazelcastHeaderEntry& other) { body_size_ = other.body_size_; @@ -73,9 +74,9 @@ HazelcastHeaderEntry::HazelcastHeaderEntry(const HazelcastHeaderEntry& other) { version_ = other.version_; } -HazelcastHeaderEntry::HazelcastHeaderEntry(HazelcastHeaderEntry&& other) : - header_map_(std::move(other.header_map_)), variant_key_(std::move(other.variant_key_)), - body_size_(other.body_size_), version_(other.version_) {} +HazelcastHeaderEntry::HazelcastHeaderEntry(HazelcastHeaderEntry&& other) + : header_map_(std::move(other.header_map_)), variant_key_(std::move(other.variant_key_)), + body_size_(other.body_size_), version_(other.version_) {} void HazelcastBodyEntry::writeData(ObjectDataOutput& writer) const { writeUnifiedData(writer); @@ -97,9 +98,9 @@ void HazelcastBodyEntry::readUnifiedData(ObjectDataInput& reader) { HazelcastBodyEntry::HazelcastBodyEntry() = default; -HazelcastBodyEntry::HazelcastBodyEntry(int64_t header_key, - std::vector&& buffer, int32_t version) : header_key_(header_key), - version_(version), body_buffer_(std::move(buffer)) {} +HazelcastBodyEntry::HazelcastBodyEntry(int64_t header_key, std::vector&& buffer, + int32_t version) + : header_key_(header_key), version_(version), body_buffer_(std::move(buffer)) {} HazelcastBodyEntry::HazelcastBodyEntry(const HazelcastBodyEntry& other) { body_buffer_ = other.body_buffer_; @@ -107,9 +108,9 @@ HazelcastBodyEntry::HazelcastBodyEntry(const HazelcastBodyEntry& other) { version_ = other.version_; } -HazelcastBodyEntry::HazelcastBodyEntry(HazelcastBodyEntry&& other) : - header_key_(other.header_key_), version_(other.version_), - body_buffer_(std::move(other.body_buffer_)) {} +HazelcastBodyEntry::HazelcastBodyEntry(HazelcastBodyEntry&& other) + : header_key_(other.header_key_), version_(other.version_), + body_buffer_(std::move(other.body_buffer_)) {} void HazelcastResponseEntry::writeData(ObjectDataOutput& writer) const { response_header_.writeUnifiedData(writer); @@ -124,11 +125,11 @@ void HazelcastResponseEntry::readData(ObjectDataInput& reader) { HazelcastResponseEntry::HazelcastResponseEntry() = default; HazelcastResponseEntry::HazelcastResponseEntry(HazelcastHeaderEntry&& header, - HazelcastBodyEntry&& body) : response_header_(std::move(header)), - response_body_(std::move(body)) {}; - -} // HazelcastHttpCache -} // Cache -} // HttpFilters -} // Extensions -} // Envoy + HazelcastBodyEntry&& body) + : response_header_(std::move(header)), response_body_(std::move(body)){}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h index c53be18ca0f93..74ff9b0e3e7ed 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h @@ -2,13 +2,12 @@ #include "common/http/header_map_impl.h" -#include "hazelcast/client/serialization/IdentifiedDataSerializable.h" -#include "hazelcast/client/serialization/ObjectDataInput.h" -#include "hazelcast/client/serialization/ObjectDataOutput.h" -#include "hazelcast/client/PartitionAware.h" - #include "source/extensions/filters/http/cache/key.pb.h" +#include "hazelcast/client/EntryView.h" +#include "hazelcast/client/PartitionAware.h" +#include "hazelcast/client/serialization/ObjectDataOutput.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -16,10 +15,10 @@ namespace Cache { namespace HazelcastHttpCache { using hazelcast::client::PartitionAware; +using hazelcast::client::serialization::DataSerializableFactory; using hazelcast::client::serialization::IdentifiedDataSerializable; -using hazelcast::client::serialization::ObjectDataOutput; using hazelcast::client::serialization::ObjectDataInput; -using hazelcast::client::serialization::DataSerializableFactory; +using hazelcast::client::serialization::ObjectDataOutput; static const int HAZELCAST_BODY_TYPE_ID = 100; static const int HAZELCAST_HEADER_TYPE_ID = 101; @@ -38,7 +37,7 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { public: HazelcastHeaderEntry(); HazelcastHeaderEntry(Http::ResponseHeaderMapPtr&& header_map, Key&& key, uint64_t body_size, - int32_t version); + int32_t version); HazelcastHeaderEntry(const HazelcastHeaderEntry& other); HazelcastHeaderEntry(HazelcastHeaderEntry&& other); @@ -80,15 +79,15 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { }; /** -* Response body wrapper for cache entries. -* -* @note In DIVIDED cache mode, response headers and corresponding bodies will be stored in -* different distributed maps. For a response HeaderEntry with 64 bit hash key , bodies -* will be stored with keys "0", "1", "2".. and so on in a contiguous manner. -* Body partition size is fixed and configurable via cache config. On a range request, only -* necessary partitions according to the request will be fetched from distributed map, -* not the whole response. -*/ + * Response body wrapper for cache entries. + * + * @note In DIVIDED cache mode, response headers and corresponding bodies will be stored in + * different distributed maps. For a response HeaderEntry with 64 bit hash key , bodies + * will be stored with keys "0", "1", "2".. and so on in a contiguous manner. + * Body partition size is fixed and configurable via cache config. On a range request, only + * necessary partitions according to the request will be fetched from distributed map, + * not the whole response. + */ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAware { public: HazelcastBodyEntry(); @@ -133,13 +132,13 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAw }; /** -* Response wrapper for cache entries. -* -* @note In UNIFIED cache mode, unlike DIVIDED, there is only one cache entry containing -* the response as a whole. Even if a range request arrives, all the body is fetched from -* the cache. This option is in favor of the efficiency of http responses with small body -* sizes. Hence it prevents extra calls for bodies after fetching header. -*/ + * Response wrapper for cache entries. + * + * @note In UNIFIED cache mode, unlike DIVIDED, there is only one cache entry containing + * the response as a whole. Even if a range request arrives, all the body is fetched from + * the cache. This option is in favor of the efficiency of http responses with small body + * sizes. Hence it prevents extra calls for bodies after fetching header. + */ class HazelcastResponseEntry : public IdentifiedDataSerializable { public: HazelcastResponseEntry(); @@ -189,8 +188,8 @@ class HazelcastCacheEntrySerializableFactory : public DataSerializableFactory { #pragma GCC diagnostic pop -} // HazelcastHttpCache -} // Cache -} // HttpFilters -} // Extensions -} // Envoy +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index 57953ed1fbc64..ab32f491bc587 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -1,8 +1,9 @@ #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" -#include "extensions/filters/http/cache/hazelcast_http_cache/config_util.h" #include "common/buffer/buffer_impl.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -11,16 +12,17 @@ namespace HazelcastHttpCache { using Envoy::Protobuf::util::MessageDifferencer; -UnifiedLookupContext::UnifiedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request) +UnifiedLookupContext::UnifiedLookupContext(HazelcastCache& cache, LookupRequest&& request) : HazelcastLookupContextBase(cache, std::move(request)) {} void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { ENVOY_LOG(debug, "Looking up unified response with key {}u", variant_hash_key_); try { response_ = hz_cache_.getResponse(variant_hash_key_); - } catch (HazelcastClientOfflineException e){ + } catch (HazelcastClientOfflineException e) { handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " - "insertions until the connection is restored...", cb); + "insertions until the connection is restored...", + cb); return; } catch (OperationTimeoutException e) { handleLookupFailure("Operation timed out during cache lookup.", cb); @@ -30,15 +32,18 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { return; } if (response_) { - ENVOY_LOG(debug, "Found unified response for key {}u, " - "body size = {}", variant_hash_key_, response_->body().length()); + ENVOY_LOG(debug, + "Found unified response for key {}u, " + "body size = {}", + variant_hash_key_, response_->body().length()); if (!MessageDifferencer::Equals(response_->header().variantKey(), variantKey())) { // As cache filter denotes, a secondary check other than the hash key // is performed here. If a different response is found with the same // hash (probably on hash collisions), the new response is denied to // be cached and the old one remains. - handleLookupFailure("Mismatched keys found for unsigned hash: " - + std::to_string(variant_hash_key_), cb, false); + handleLookupFailure("Mismatched keys found for unsigned hash: " + + std::to_string(variant_hash_key_), + cb, false); return; } cb(lookup_request_.makeLookupResult(std::move(response_->header().headerMap()), @@ -56,15 +61,15 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { void UnifiedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) { ENVOY_LOG(debug, "Getting unified body (total length = {}) with range from {} to {}", - response_->body().length(), range.begin(), range.end()); + response_->body().length(), range.begin(), range.end()); ASSERT(response_ && !abort_insertion_); ASSERT(range.end() <= response_->body().length()); hazelcast::byte* data = response_->body().begin() + range.begin(); cb(std::make_unique(data, range.length())); } -UnifiedInsertContext::UnifiedInsertContext(LookupContext& lookup_context, - HazelcastHttpCache& cache) : HazelcastInsertContextBase(lookup_context, cache) {} +UnifiedInsertContext::UnifiedInsertContext(LookupContext& lookup_context, HazelcastCache& cache) + : HazelcastInsertContextBase(lookup_context, cache) {} void UnifiedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) { @@ -116,7 +121,7 @@ void UnifiedInsertContext::flushEntry() { // Versions are not necessary for unified entries. Hence passing arbitrary 0 here. HazelcastHeaderEntry header(std::move(header_map_), std::move(variant_key_), - buffer_vector_.size(), 0); + buffer_vector_.size(), 0); HazelcastBodyEntry body(variant_hash_key_, std::move(buffer_vector_), 0); HazelcastResponseEntry entry(std::move(header), std::move(body)); @@ -131,18 +136,19 @@ void UnifiedInsertContext::flushEntry() { } } -DividedLookupContext::DividedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request) +DividedLookupContext::DividedLookupContext(HazelcastCache& cache, LookupRequest&& request) : HazelcastLookupContextBase(cache, std::move(request)), - body_partition_size_(cache.bodySizePerEntry()) {}; + body_partition_size_(cache.bodySizePerEntry()){}; void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { ENVOY_LOG(debug, "Looking up divided header with key {}u", variant_hash_key_); HazelcastHeaderPtr header_entry; try { header_entry = hz_cache_.getHeader(variant_hash_key_); - } catch (HazelcastClientOfflineException e){ + } catch (HazelcastClientOfflineException e) { handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " - "insertions until the connection is restored.", cb); + "insertions until the connection is restored.", + cb); return; } catch (OperationTimeoutException e) { handleLookupFailure("Operation timed out during cache lookup.", cb); @@ -154,10 +160,11 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { if (header_entry) { abort_insertion_ = true; // overriding an exiting entry is not allowed. ENVOY_LOG(debug, "Found divided response for key {}u, version {}, body size = {}", - variant_hash_key_, header_entry->version(), header_entry->bodySize()); + variant_hash_key_, header_entry->version(), header_entry->bodySize()); if (!MessageDifferencer::Equals(header_entry->variantKey(), variantKey())) { - handleLookupFailure("Mismatched keys found for unsigned hash: " - + std::to_string(variant_hash_key_), cb, false); + handleLookupFailure("Mismatched keys found for unsigned hash: " + + std::to_string(variant_hash_key_), + cb, false); return; } this->total_body_size_ = header_entry->bodySize(); @@ -176,7 +183,8 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { abort_insertion_ = !hz_cache_.tryLock(variant_hash_key_); } catch (HazelcastClientOfflineException e) { handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and insertions" - " until the connection is restored...", cb); + " until the connection is restored...", + cb); return; } catch (...) { handleLookupFailure("Lock trial has failed.", cb); @@ -210,7 +218,8 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal body = hz_cache_.getBody(variant_hash_key_, body_index); } catch (HazelcastClientOfflineException e) { handleBodyLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " - "insertions until the connection is restored...", cb); + "insertions until the connection is restored...", + cb); return; } catch (OperationTimeoutException e) { handleBodyLookupFailure("Operation timed out during cache lookup.", cb); @@ -222,12 +231,14 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal if (body) { ENVOY_LOG(debug, "Found divided body with key {}u + \"{}\", version {}, size {}", - variant_hash_key_, body_index, body->version(),body->length()); + variant_hash_key_, body_index, body->version(), body->length()); if (body->version() != version_) { hz_cache_.onVersionMismatch(variant_hash_key_, version_, total_body_size_); - handleBodyLookupFailure(fmt::format("Body version mismatched with header for " - "key {}u at body: {}. Aborting lookup and performing cleanup.", variant_hash_key_, - body_index), cb, false); + handleBodyLookupFailure( + fmt::format("Body version mismatched with header for " + "key {}u at body: {}. Aborting lookup and performing cleanup.", + variant_hash_key_, body_index), + cb, false); return; } uint64_t offset = (range.begin() % body_partition_size_); @@ -245,13 +256,14 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal // Body partition is expected to reside in the cache but lookup is failed. hz_cache_.onMissingBody(variant_hash_key_, version_, total_body_size_); handleBodyLookupFailure(fmt::format("Found missing body for key {}u at index: {}. Response " - "with body size {} has been cleaned up from the cache.",variant_hash_key_, body_index, - total_body_size_), cb, false); + "with body size {} has been cleaned up from the cache.", + variant_hash_key_, body_index, total_body_size_), + cb, false); } }; void DividedLookupContext::handleBodyLookupFailure(absl::string_view message, - const LookupBodyCallback& cb, bool warn_log){ + const LookupBodyCallback& cb, bool warn_log) { if (warn_log) { ENVOY_LOG(warn, "{}", message); } else { @@ -260,12 +272,12 @@ void DividedLookupContext::handleBodyLookupFailure(absl::string_view message, cb(nullptr); } -DividedInsertContext::DividedInsertContext(LookupContext& lookup_context, - HazelcastHttpCache& cache) : HazelcastInsertContextBase(lookup_context, cache), - body_partition_size_(cache.bodySizePerEntry()), version_(createVersion()) {} +DividedInsertContext::DividedInsertContext(LookupContext& lookup_context, HazelcastCache& cache) + : HazelcastInsertContextBase(lookup_context, cache), + body_partition_size_(cache.bodySizePerEntry()), version_(createVersion()) {} void DividedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response_headers, - bool end_stream) { + bool end_stream) { if (abort_insertion_) { return; } @@ -281,7 +293,7 @@ void DividedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response // content is copied into a local buffer every time insertBody is called. And it is // flushed when it reaches the maximum capacity (body_partition_size_). void DividedInsertContext::insertBody(const Buffer::Instance& chunk, - InsertCallback ready_for_next_chunk, bool end_stream) { + InsertCallback ready_for_next_chunk, bool end_stream) { if (abort_insertion_) { ENVOY_LOG(debug, "Aborting insertion for the hash key: {}", variant_hash_key_); if (ready_for_next_chunk) { @@ -291,8 +303,8 @@ void DividedInsertContext::insertBody(const Buffer::Instance& chunk, } ASSERT(!committed_end_stream_); uint64_t copied_bytes = 0; - uint64_t allowed_bytes = max_body_size_ - - (body_order_ * body_partition_size_ + buffer_vector_.size()); + uint64_t allowed_bytes = + max_body_size_ - (body_order_ * body_partition_size_ + buffer_vector_.size()); uint64_t remaining_bytes = allowed_bytes < chunk.length() ? allowed_bytes : chunk.length(); bool trimmed = remaining_bytes == allowed_bytes; while (remaining_bytes) { @@ -334,7 +346,7 @@ void DividedInsertContext::insertBody(const Buffer::Instance& chunk, } void DividedInsertContext::copyIntoLocalBuffer(uint64_t& offset, uint64_t& size, - const Buffer::Instance& source) { + const Buffer::Instance& source) { uint64_t current_size = buffer_vector_.size(); buffer_vector_.resize(current_size + size); source.copyOut(offset, size, buffer_vector_.data() + current_size); @@ -353,8 +365,8 @@ bool DividedInsertContext::flushBuffer() { return true; } total_body_size_ += buffer_vector_.size(); - HazelcastBodyEntry bodyEntry(hz_cache_.mapKey(variant_hash_key_), - std::move(buffer_vector_), version_); + HazelcastBodyEntry bodyEntry(hz_cache_.mapKey(variant_hash_key_), std::move(buffer_vector_), + version_); buffer_vector_.clear(); try { hz_cache_.putBody(variant_hash_key_, body_order_++, bodyEntry); @@ -370,7 +382,7 @@ bool DividedInsertContext::flushBuffer() { } if (body_order_ == ConfigUtil::partitionWarnLimit()) { ENVOY_LOG(warn, "Number of body partitions for a response has been reached {} (or more).", - ConfigUtil::partitionWarnLimit()); + ConfigUtil::partitionWarnLimit()); ENVOY_LOG(info, "Having so many partitions might cause performance drop " "as well as extra memory usage. Consider increasing body " "partition size."); @@ -382,8 +394,8 @@ void DividedInsertContext::flushHeader() { ASSERT(!abort_insertion_); ASSERT(!committed_end_stream_); committed_end_stream_ = true; - HazelcastHeaderEntry header(std::move(header_map_), std::move(variant_key_), - total_body_size_, version_); + HazelcastHeaderEntry header(std::move(header_map_), std::move(variant_key_), total_body_size_, + version_); try { hz_cache_.putHeader(variant_hash_key_, header); hz_cache_.unlock(variant_hash_key_); @@ -414,8 +426,8 @@ int32_t DividedInsertContext::createVersion() { return (rand64 % (max * 2)) - max; } -} // HazelcastHttpCache -} // Cache -} // HttpFilters -} // Extensions -} // Envoy +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h index eb3505de2f827..9e44927bf39f2 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h @@ -1,6 +1,6 @@ #pragma once -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" namespace Envoy { namespace Extensions { @@ -14,7 +14,7 @@ namespace HazelcastHttpCache { class HazelcastLookupContextBase : public LookupContext, public Logger::Loggable { public: - HazelcastLookupContextBase(HazelcastHttpCache& cache, LookupRequest&& request) + HazelcastLookupContextBase(HazelcastCache& cache, LookupRequest&& request) : hz_cache_(cache), lookup_request_(std::move(request)) { createVariantKey(lookup_request_.key()); variant_hash_key_ = stableHashKey(lookup_request_.key()); @@ -34,9 +34,8 @@ class HazelcastLookupContextBase : public LookupContext, bool isAborted() const { return abort_insertion_; } protected: - void handleLookupFailure(absl::string_view message, const LookupHeadersCallback& cb, - bool warn_log = true) { + bool warn_log = true) { if (warn_log) { ENVOY_LOG(warn, "{}", message); } else { @@ -46,7 +45,7 @@ class HazelcastLookupContextBase : public LookupContext, cb(LookupResult{}); } - HazelcastHttpCache& hz_cache_; + HazelcastCache& hz_cache_; LookupRequest lookup_request_; /** Hash key aware of vary headers. Lookup will be performed using this. */ @@ -56,7 +55,6 @@ class HazelcastLookupContextBase : public LookupContext, bool abort_insertion_ = false; private: - /** * The keys created by the cache filter for lookups and inserts are not aware * of the vary headers of the request. Instead, cache filter expects a @@ -78,7 +76,7 @@ class HazelcastLookupContextBase : public LookupContext, for (const Http::HeaderEntry& header : lookup_request_.vary_headers()) { header_strings.push_back(std::make_pair(std::string(header.key().getStringView()), - std::string(header.value().getStringView()))); + std::string(header.value().getStringView()))); } // Different order of headers causes different hash keys even if their both key and value @@ -113,7 +111,6 @@ class HazelcastLookupContextBase : public LookupContext, // - {accept-language: en-US,tr;q=0.8} // - {accept-language: tr;q=0.8,en-US} } - }; /** @@ -122,11 +119,12 @@ class HazelcastLookupContextBase : public LookupContext, class HazelcastInsertContextBase : public InsertContext, public Logger::Loggable { public: - HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastHttpCache& cache) + HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastCache& cache) : hz_cache_(cache), max_body_size_(cache.maxBodySize()), - variant_hash_key_(static_cast(lookup_context).variantHashKey()), - variant_key_(static_cast(lookup_context).variantKey()), - abort_insertion_(static_cast(lookup_context).isAborted()) {} + variant_hash_key_( + static_cast(lookup_context).variantHashKey()), + variant_key_(static_cast(lookup_context).variantKey()), + abort_insertion_(static_cast(lookup_context).isAborted()) {} void insertTrailers(const Http::ResponseTrailerMap&) override { // TODO(enozcan): Support trailers @@ -134,7 +132,7 @@ class HazelcastInsertContextBase : public InsertContext, } protected: - HazelcastHttpCache& hz_cache_; + HazelcastCache& hz_cache_; const uint64_t max_body_size_; bool committed_end_stream_ = false; @@ -155,9 +153,10 @@ class HazelcastInsertContextBase : public InsertContext, */ class UnifiedLookupContext : public HazelcastLookupContextBase { public: - UnifiedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request); + UnifiedLookupContext(HazelcastCache& cache, LookupRequest&& request); void getHeaders(LookupHeadersCallback&& cb) override; void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; + private: HazelcastResponsePtr response_; }; @@ -167,10 +166,11 @@ class UnifiedLookupContext : public HazelcastLookupContextBase { */ class UnifiedInsertContext : public HazelcastInsertContextBase { public: - UnifiedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache); + UnifiedInsertContext(LookupContext& lookup_context, HazelcastCache& cache); void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) override; void insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, bool end_stream) override; + private: void flushEntry(); }; @@ -180,13 +180,13 @@ class UnifiedInsertContext : public HazelcastInsertContextBase { */ class DividedLookupContext : public HazelcastLookupContextBase { public: - DividedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request); + DividedLookupContext(HazelcastCache& cache, LookupRequest&& request); void getHeaders(LookupHeadersCallback&& cb) override; void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; private: void handleBodyLookupFailure(absl::string_view message, const LookupBodyCallback& cb, - bool warn_log = true); + bool warn_log = true); uint64_t total_body_size_; @@ -203,7 +203,7 @@ class DividedLookupContext : public HazelcastLookupContextBase { */ class DividedInsertContext : public HazelcastInsertContextBase { public: - DividedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache); + DividedInsertContext(LookupContext& lookup_context, HazelcastCache& cache); void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) override; void insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, bool end_stream) override; @@ -229,11 +229,10 @@ class DividedInsertContext : public HazelcastInsertContextBase { const int32_t version_; uint64_t total_body_size_ = 0; - }; -} // HazelcastHttpCache -} // Cache -} // HttpFilters -} // Extensions -} // Envoy +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h deleted file mode 100644 index 2fb738716ec3f..0000000000000 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once - -#include "common/common/logger.h" -#include "common/runtime/runtime_impl.h" - -#include "extensions/filters/http/cache/http_cache.h" -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" - -#include "hazelcast/client/HazelcastClient.h" -#include "hazelcast/client/IMap.h" - -#include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" - -namespace Envoy { -namespace Extensions { -namespace HttpFilters { -namespace Cache { -namespace HazelcastHttpCache { - -using hazelcast::client::IMap; -using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; - -// TODO: Consider putting responses into cache with TTL derived from `age` header. -// instead of using a common TTL for all. - -// TODO: Mention about max.lease.time option on readme doc. - -class HazelcastHttpCache : public HttpCache, - public Logger::Loggable { -public: - HazelcastHttpCache(HazelcastHttpCacheConfig config); - - // Cache::HttpCache - LookupContextPtr makeLookupContext(LookupRequest&& request) override; - InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; - void updateHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) override; - CacheInfo cacheInfo() const override; - - // Divided profile - void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry); - void putBody(const uint64_t& key, const uint64_t& order, const HazelcastBodyEntry& entry); - HazelcastHeaderPtr getHeader(const uint64_t& key); - HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order); - - // Unified profile - void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry); - HazelcastResponsePtr getResponse(const uint64_t& key); - - // Hazelcast cluster connection - void connect(); - void shutdown(bool destroy); - - // Recoveries for malformed entries - void onMissingBody(uint64_t key, int32_t version, uint64_t body_size); - void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size); - - // Internal lock mechanism of Hazelcast specific to map and key pair is - // used to make exactly one lookup context responsible for insertions and - // secure consistency during updateHeaders(). These locks prevent possible - // race for multiple cache filters from multiple proxies when they connect - // to the same Hazelcast cluster. - bool tryLock(const uint64_t& key); - void unlock(const uint64_t& key); - - const uint64_t& bodySizePerEntry() { return body_partition_size_; }; - const uint64_t& maxBodySize() { return max_body_size_; }; - - inline int64_t mapKey(const uint64_t& unsigned_key) { - // Hazelcast client accepts signed keys on maps. The reason for not static casting - // directly is a possible overflow for int64 on intermediate step for -2^63. - int64_t signed_key; - std::memcpy (&signed_key, &unsigned_key, sizeof(int64_t)); - return signed_key; - } - - /** - * Creates string keys for body partition entries. - * - * @param key Unsigned hash key for the header. - * @param order Order of the body among other partitions starting from 0. - * @return Body partition key for header and order pair. - * - * @note Appending '#' or any other marker between the key and order - * string is required. Otherwise, for instance, the 11th order - * body for key 1 and the 1st order body for key 11 will have - * the same map key "111". - */ - inline std::string orderedMapKey(const uint64_t& key, const uint64_t& order) { - return std::to_string(key).append("#").append(std::to_string(order)); - } - - uint64_t random() { - return rand_.random(); - } - - ~HazelcastHttpCache(); - -private: - friend class HazelcastHttpCacheTestBase; - - void updateUnifiedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers); - void updateDividedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers); - - // Maps are differentiated by their names in Hazelcast cluster. Hence each - // plugin will connect to a map named with partition size and app_prefix. - // When a cache connects to a cluster which already has an active cache - // with different body_partition_size, this naming will prevent incompatibility - // and separate these two caches in the Hazelcast cluster. - std::string constructMapName(const std::string& postfix); - - inline IMap getHeaderMap() { - return hazelcast_client_->getMap(header_map_name_); - } - - inline IMap getBodyMap() { - return hazelcast_client_->getMap(body_map_name_); - } - - inline IMap getResponseMap() { - return hazelcast_client_->getMap(response_map_name_); - } - - std::unique_ptr hazelcast_client_; - HazelcastHttpCacheConfig cache_config_; - - // From cache configuration - const bool unified_; - const uint64_t body_partition_size_; - const uint64_t max_body_size_; - - std::string body_map_name_; - std::string header_map_name_; - std::string response_map_name_; - - Runtime::RandomGeneratorImpl rand_; -}; - -} // namespace HazelcastHttpCache -} // namespace Cache -} // namespace HttpFilters -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc similarity index 72% rename from source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc rename to source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc index 61e8a424f4655..6e953af6ba99c 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc @@ -1,8 +1,7 @@ -#include "extensions/filters/http/cache/hazelcast_http_cache/config_util.h" -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" -#include "envoy/registry/registry.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" namespace Envoy { namespace Extensions { @@ -28,7 +27,7 @@ InsertContextPtr HazelcastHttpCache::makeInsertContext(LookupContextPtr&& lookup } void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) { + Http::ResponseHeaderMapPtr&& response_headers) { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; // TODO(enozcan): Enable when implemented on the filter side. // Depending on the filter's implementation, the cached entry's @@ -69,7 +68,7 @@ void HazelcastHttpCache::putHeader(const uint64_t& key, const HazelcastHeaderEnt } void HazelcastHttpCache::putBody(const uint64_t& key, const uint64_t& order, - const HazelcastBodyEntry& entry) { + const HazelcastBodyEntry& entry) { getBodyMap().set(orderedMapKey(key, order), entry); } @@ -81,59 +80,7 @@ HazelcastBodyPtr HazelcastHttpCache::getBody(const uint64_t& key, const uint64_t return getBodyMap().get(orderedMapKey(key, order)); } -void HazelcastHttpCache::putResponseIfAbsent(const uint64_t& key, - const HazelcastResponseEntry& entry) { - getResponseMap().putIfAbsent(mapKey(key), entry); -} - -HazelcastResponsePtr HazelcastHttpCache::getResponse(const uint64_t& key) { - return getResponseMap().get(mapKey(key)); -} - -void HazelcastHttpCache::connect() { - if (hazelcast_client_ && hazelcast_client_->getLifecycleService().isRunning()) { - ENVOY_LOG(warn, "Client is already connected. Cluster name: {}", - hazelcast_client_->getClientConfig().getGroupConfig().getName()); - return; - } - - ClientConfig config = ConfigUtil::getClientConfig(cache_config_); - config.getSerializationConfig().addDataSerializableFactory( - HazelcastCacheEntrySerializableFactory::FACTORY_ID, - boost::shared_ptr - (new HazelcastCacheEntrySerializableFactory())); - - try { - hazelcast_client_ = std::make_unique(config); - } catch (...) { - throw EnvoyException("Hazelcast Client could not connect to any cluster."); - } - - ENVOY_LOG(info, "HazelcastHttpCache has been started with profile: {}. Max body size: {}.", - unified_ ? "UNIFIED" : "DIVIDED, partition size: " + std::to_string(body_partition_size_), - max_body_size_); - ENVOY_LOG(info, "Cache statistics can be observed on Hazelcast Management Center" - " from the map named {}.", unified_ ? response_map_name_ : header_map_name_); -} - -void HazelcastHttpCache::shutdown(bool destroy) { - if (!hazelcast_client_) { - ENVOY_LOG(warn, "Client is already offline."); - return; - } - if (hazelcast_client_->getLifecycleService().isRunning()) { - ENVOY_LOG(info, "Shutting down Hazelcast connection..."); - hazelcast_client_->shutdown(); - ENVOY_LOG(info, "Cache is offline now."); - } else { - ENVOY_LOG(warn, "Cache is already offline."); - } - if (destroy) { - hazelcast_client_.reset(); - } -} - -void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { +void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { try { if (!tryLock(key)) { // Let lock owner context to recover it. @@ -168,9 +115,22 @@ void HazelcastHttpCache::onVersionMismatch(uint64_t key, int32_t version, uint64 onMissingBody(key, version, body_size); } +void HazelcastHttpCache::putResponseIfAbsent(const uint64_t& key, + const HazelcastResponseEntry& entry) { + getResponseMap().putIfAbsent(mapKey(key), entry); +} + +HazelcastResponsePtr HazelcastHttpCache::getResponse(const uint64_t& key) { + return getResponseMap().get(mapKey(key)); +} + bool HazelcastHttpCache::tryLock(const uint64_t& key) { - return unified_ ? getResponseMap().tryLock(mapKey(key)) : - getHeaderMap().tryLock(mapKey(key)); + // Internal lock mechanism of Hazelcast specific to map and key pair is + // used to make exactly one lookup context responsible for insertions and + // secure consistency during updateHeaders(). These locks prevent possible + // race for multiple cache filters from multiple proxies when they connect + // to the same Hazelcast cluster. + return unified_ ? getResponseMap().tryLock(mapKey(key)) : getHeaderMap().tryLock(mapKey(key)); } void HazelcastHttpCache::unlock(const uint64_t& key) { @@ -183,12 +143,58 @@ void HazelcastHttpCache::unlock(const uint64_t& key) { } } -HazelcastHttpCache::~HazelcastHttpCache() { - shutdown(true); +uint64_t HazelcastHttpCache::random() { return rand_.random(); } + +void HazelcastHttpCache::connect() { + if (hazelcast_client_ && hazelcast_client_->getLifecycleService().isRunning()) { + ENVOY_LOG(warn, "Client is already connected. Cluster name: {}", + hazelcast_client_->getClientConfig().getGroupConfig().getName()); + return; + } + + ClientConfig config = ConfigUtil::getClientConfig(cache_config_); + config.getSerializationConfig().addDataSerializableFactory( + HazelcastCacheEntrySerializableFactory::FACTORY_ID, + boost::shared_ptr( + new HazelcastCacheEntrySerializableFactory())); + + try { + hazelcast_client_ = std::make_unique(config); + } catch (...) { + throw EnvoyException("Hazelcast Client could not connect to any cluster."); + } + + ENVOY_LOG(info, "HazelcastHttpCache has been started with profile: {}. Max body size: {}.", + unified_ ? "UNIFIED" + : "DIVIDED, partition size: " + std::to_string(body_partition_size_), + max_body_size_); + ENVOY_LOG(info, + "Cache statistics can be observed on Hazelcast Management Center" + " from the map named {}.", + unified_ ? response_map_name_ : header_map_name_); } +void HazelcastHttpCache::shutdown(bool destroy) { + if (!hazelcast_client_) { + ENVOY_LOG(warn, "Client is already offline."); + return; + } + if (hazelcast_client_->getLifecycleService().isRunning()) { + ENVOY_LOG(info, "Shutting down Hazelcast connection..."); + hazelcast_client_->shutdown(); + ENVOY_LOG(info, "Cache is offline now."); + } else { + ENVOY_LOG(warn, "Cache is already offline."); + } + if (destroy) { + hazelcast_client_.reset(); + } +} + +HazelcastHttpCache::~HazelcastHttpCache() { shutdown(true); } + void HazelcastHttpCache::updateUnifiedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) { + Http::ResponseHeaderMapPtr&& response_headers) { const uint64_t& key = static_cast(lookup_context.get())->variantHashKey(); HazelcastResponsePtr response = getResponse(key); if (!response) { @@ -203,7 +209,7 @@ void HazelcastHttpCache::updateUnifiedHeaders(LookupContextPtr&& lookup_context, } void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) { + Http::ResponseHeaderMapPtr&& response_headers) { const uint64_t& key = static_cast(lookup_context.get())->variantHashKey(); HazelcastHeaderPtr stale = getHeader(key); if (!stale) { @@ -218,6 +224,11 @@ void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, } std::string HazelcastHttpCache::constructMapName(const std::string& postfix) { + // Maps are differentiated by their names in Hazelcast cluster. Hence each + // plugin will connect to a map named with partition size and app_prefix. + // When a cache connects to a cluster which already has an active cache + // with different body_partition_size, this naming will prevent incompatibility + // and separate these two caches in the Hazelcast cluster. std::string name(cache_config_.app_prefix()); if (!unified_) { name.append(":").append(std::to_string(body_partition_size_)); @@ -226,34 +237,41 @@ std::string HazelcastHttpCache::constructMapName(const std::string& postfix) { } HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) - : cache_config_(config), unified_(config.unified()), - body_partition_size_(ConfigUtil::validPartitionSize(config.body_partition_size())), - max_body_size_(ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())) { + : HazelcastCache(config.unified(), ConfigUtil::validPartitionSize(config.body_partition_size()), + ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())), + cache_config_(config) { body_map_name_ = constructMapName("body"); header_map_name_ = constructMapName("div-cache"); response_map_name_ = constructMapName("uni-cache"); } -class HazelcastHttpCacheFactory : public HttpCacheFactory { -public: - // From UntypedFactory - std::string name() const override { return std::string(HazelcastCacheName); } - // From TypedFactory - ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); +std::string HazelcastHttpCacheFactory::name() const { return std::string(HazelcastCacheName); } + +ProtobufTypes::MessagePtr HazelcastHttpCacheFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +HttpCache& HazelcastHttpCacheFactory::getCache( + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { + if (!cache_) { + HazelcastHttpCacheConfig hz_cache_config; + MessageUtil::unpackTo(config.typed_config(), hz_cache_config); + cache_ = std::make_unique(hz_cache_config); } - // From HttpCacheFactory - HttpCache& - getCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) override { + cache_->connect(); + return *cache_; +} + +HttpCache& HazelcastHttpCacheFactory::getOfflineCache( + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { + if (!cache_) { HazelcastHttpCacheConfig hz_cache_config; MessageUtil::unpackTo(config.typed_config(), hz_cache_config); cache_ = std::make_unique(hz_cache_config); - cache_->connect(); - return *cache_; } -private: - std::unique_ptr cache_; -}; + cache_->shutdown(false); + return *cache_; +} static Registry::RegisterFactory register_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h new file mode 100644 index 0000000000000..3724b90921973 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h @@ -0,0 +1,121 @@ +#pragma once + +#include "envoy/registry/registry.h" + +#include "common/common/logger.h" +#include "common/runtime/runtime_impl.h" + +#include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h" + +#include "hazelcast/client/HazelcastClient.h" +#include "hazelcast/client/IMap.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; +using hazelcast::client::IMap; + +// TODO: Consider putting responses into cache with TTL derived from `age` header. +// instead of using a common TTL for all. + +// TODO: Mention about max.lease.time option on readme doc. + +class HazelcastHttpCache : /*public HttpCache,*/ public HazelcastCache, + public Logger::Loggable { +public: + HazelcastHttpCache(HazelcastHttpCacheConfig config); + + // Cache::HttpCache + LookupContextPtr makeLookupContext(LookupRequest&& request) override; + InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; + void updateHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) override; + CacheInfo cacheInfo() const override; + + // HazelcastCache + void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) override; + void putBody(const uint64_t& key, const uint64_t& order, + const HazelcastBodyEntry& entry) override; + HazelcastHeaderPtr getHeader(const uint64_t& key) override; + HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order) override; + void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) override; + void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) override; + void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry) override; + HazelcastResponsePtr getResponse(const uint64_t& key) override; + bool tryLock(const uint64_t& key) override; + void unlock(const uint64_t& key) override; + uint64_t random() override; + + void connect(); + void shutdown(bool destroy); + + ~HazelcastHttpCache(); + +private: + friend class HazelcastHttpCacheTestBase; + friend class HazelcastTestableRemoteCache; + + void updateUnifiedHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers); + + void updateDividedHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers); + + /** Generates a unique map name to this cache with the given postfix. */ + std::string constructMapName(const std::string& postfix); + + /** Returns remote header cache proxy */ + inline IMap getHeaderMap() { + return hazelcast_client_->getMap(header_map_name_); + } + + /** Returns remote body cache proxy */ + inline IMap getBodyMap() { + return hazelcast_client_->getMap(body_map_name_); + } + + /** Returns remote response cache proxy */ + inline IMap getResponseMap() { + return hazelcast_client_->getMap(response_map_name_); + } + + std::unique_ptr hazelcast_client_; + HazelcastHttpCacheConfig cache_config_; + + std::string body_map_name_; + std::string header_map_name_; + std::string response_map_name_; + + Runtime::RandomGeneratorImpl rand_; +}; + +class HazelcastHttpCacheFactory : public HttpCacheFactory { +public: + // UntypedFactory + std::string name() const override; + + // TypedFactory + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + // HttpCacheFactory + HttpCache& + getCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) override; + + HttpCache& // For testing only. + getOfflineCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config); + +private: + std::unique_ptr cache_; +}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h similarity index 55% rename from source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h rename to source/extensions/filters/http/cache/hazelcast_http_cache/util.h index 64447e012e5e0..a4f1b3b38c96f 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config_util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -1,6 +1,7 @@ #pragma once #include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" + #include "hazelcast/client/ClientConfig.h" namespace Envoy { @@ -11,11 +12,10 @@ namespace HazelcastHttpCache { class ConfigUtil { public: - static uint64_t validPartitionSize(const uint64_t config_value) { - return config_value == 0 ? - DEFAULT_PARTITION_SIZE : (config_value > MAX_PARTITION_SIZE) ? - MAX_PARTITION_SIZE : config_value; + return config_value == 0 + ? DEFAULT_PARTITION_SIZE + : (config_value > MAX_PARTITION_SIZE) ? MAX_PARTITION_SIZE : config_value; } static uint64_t validMaxBodySize(const uint64_t config_value, const bool unified) { @@ -23,31 +23,34 @@ class ConfigUtil { return config_value == 0 || (config_value > max_size) ? max_size : config_value; } - static hazelcast::client::ClientConfig getClientConfig(const - envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig& cache_config) { + static hazelcast::client::ClientConfig + getClientConfig(const envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig& + cache_config) { hazelcast::client::ClientConfig config; config.getGroupConfig().setName(cache_config.group_name()); - config.getNetworkConfig().setConnectionTimeout(cache_config.connection_timeout() - == 0 ? DEFAULT_CONNECTION_TIMEOUT_MS : cache_config.connection_timeout()); - config.getNetworkConfig().setConnectionAttemptLimit(cache_config.connection_attempt_limit() - == 0 ? DEFAULT_CONNECTION_ATTEMPT_LIMIT : cache_config.connection_attempt_limit()); - config.getNetworkConfig().setConnectionAttemptPeriod(cache_config.connection_attempt_period() - == 0 ? DEFAULT_CONNECTION_ATTEMPT_PERIOD_MS : cache_config.connection_attempt_period()); + config.getNetworkConfig().setConnectionTimeout(cache_config.connection_timeout() == 0 + ? DEFAULT_CONNECTION_TIMEOUT_MS + : cache_config.connection_timeout()); + config.getNetworkConfig().setConnectionAttemptLimit( + cache_config.connection_attempt_limit() == 0 ? DEFAULT_CONNECTION_ATTEMPT_LIMIT + : cache_config.connection_attempt_limit()); + config.getNetworkConfig().setConnectionAttemptPeriod( + cache_config.connection_attempt_period() == 0 ? DEFAULT_CONNECTION_ATTEMPT_PERIOD_MS + : cache_config.connection_attempt_period()); config.getConnectionStrategyConfig().setReconnectMode( - hazelcast::client::config::ClientConnectionStrategyConfig::ReconnectMode::ASYNC); - for (auto &address : cache_config.addresses()) { - config.getNetworkConfig().addAddress(hazelcast::client::Address(address.ip(), - address.port())); + hazelcast::client::config::ClientConnectionStrategyConfig::ReconnectMode::ASYNC); + for (auto& address : cache_config.addresses()) { + config.getNetworkConfig().addAddress( + hazelcast::client::Address(address.ip(), address.port())); } config.setProperty("hazelcast.client.invocation.timeout.seconds", - std::to_string(cache_config.invocation_timeout() == 0 ? - DEFAULT_INVOCATION_TIMEOUT_SEC : cache_config.invocation_timeout())); + std::to_string(cache_config.invocation_timeout() == 0 + ? DEFAULT_INVOCATION_TIMEOUT_SEC + : cache_config.invocation_timeout())); return config; } - static short partitionWarnLimit() { - return WARN_PARTITION_LIMIT; - } + static short partitionWarnLimit() { return WARN_PARTITION_LIMIT; } private: // After this much body partitions stored for a response in DIVIDED mode, @@ -78,8 +81,8 @@ class ConfigUtil { static constexpr uint32_t DEFAULT_INVOCATION_TIMEOUT_SEC = 8; }; -} // HazelcastHttpCache -} // Cache -} // HttpFilters -} // Extensions -} // Envoy +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 7810082c1ba9c..ef5d6ef7eeb29 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -14,7 +14,7 @@ envoy_extension_cc_test( srcs = ["hazelcast_unified_cache_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ - ":hazelcast_test_util_lib", + ":hazelcast_test_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", ], @@ -25,7 +25,7 @@ envoy_extension_cc_test( srcs = ["hazelcast_divided_cache_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ - ":hazelcast_test_util_lib", + ":hazelcast_test_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", ], @@ -36,16 +36,19 @@ envoy_extension_cc_test( srcs = ["hazelcast_common_cache_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ - ":hazelcast_test_util_lib", + ":hazelcast_test_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", ], ) - envoy_extension_cc_test_library( - name = "hazelcast_test_util_lib", - hdrs = ["util.h"], + name = "hazelcast_test_lib", + srcs = ["hazelcast_test_cache.cc"], + hdrs = [ + "hazelcast_test_cache.h", + "util.h", + ], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ "//source/extensions/filters/http/cache/hazelcast_http_cache:hazelcast_http_cache_lib", diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index cc4d949a8d20f..2d674c4f46312 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -1,6 +1,7 @@ -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" #include "envoy/registry/registry.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" + #include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" namespace Envoy { @@ -15,12 +16,14 @@ namespace HazelcastHttpCache { class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, public testing::WithParamInterface { protected: - void SetUp() { HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(GetParam()); - cache_ = new HazelcastHttpCache(cfg); - cache_->connect(); - clearMaps(); + // To test the cache with a real Hazelcast instance, remote cache + // must be used during tests. + // cache_ = std::make_unique(cfg); + cache_ = std::make_unique(cfg); + cache_->restoreTestConnection(); + cache_->clearTestMaps(); } }; @@ -68,24 +71,22 @@ TEST_P(HazelcastHttpCacheTest, HandleRangedResponses) { lookup_context1 = lookup(RequestPath); // 'h' * (size) - EXPECT_EQ(absl::string_view(Body.c_str(), size), - getBody(*lookup_context1, 0, size)); + EXPECT_EQ(absl::string_view(Body.c_str(), size), getBody(*lookup_context1, 0, size)); // 'z' * (size) EXPECT_EQ(absl::string_view(Body.c_str() + size, size), - getBody(*lookup_context1, size, size * 2)); + getBody(*lookup_context1, size, size * 2)); // 'h' * (size/2) + 'z' * (size/2) EXPECT_EQ(absl::string_view(Body.c_str() + size / 2, size), - getBody(*lookup_context1, size / 2, size + size / 2)); + getBody(*lookup_context1, size / 2, size + size / 2)); // 'h' + 'z' * (size) + 'c' EXPECT_EQ(absl::string_view(Body.c_str() + size - 1, size + 2), - getBody(*lookup_context1, size - 1, 2 * size + 1)); + getBody(*lookup_context1, size - 1, 2 * size + 1)); // 'h' * (size) + 'z' * (size) + 'c' * (size) - EXPECT_EQ(absl::string_view(Body.c_str(), size * 3), - getBody(*lookup_context1, 0, size * 3)); + EXPECT_EQ(absl::string_view(Body.c_str(), size * 3), getBody(*lookup_context1, 0, size * 3)); } // @@ -124,11 +125,11 @@ TEST_P(HazelcastHttpCacheTest, PrivateResponse) { TEST_P(HazelcastHttpCacheTest, Miss) { LookupContextPtr name_lookup_context = lookup("/no/such/entry"); uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); + static_cast(*name_lookup_context).variantHashKey(); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // Do not left over a missed lookup without inserting or releasing its lock. - unlockKey(variant_hash_key); + cache_->base().unlock(variant_hash_key); } TEST_P(HazelcastHttpCacheTest, Fresh) { @@ -151,10 +152,9 @@ TEST_P(HazelcastHttpCacheTest, RequestSmallMinFresh) { LookupContextPtr name_lookup_context = lookup(request_path); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - Http::TestResponseHeaderMapImpl response_headers{ - {"date", formatter_.fromTime(current_time_)}, - {"age", "6000"}, - {"cache-control", "public, max-age=9000"}}; + Http::TestResponseHeaderMapImpl response_headers{{"date", formatter_.fromTime(current_time_)}, + {"age", "6000"}, + {"cache-control", "public, max-age=9000"}}; const std::string Body("content"); insert(move(name_lookup_context), response_headers, Body); EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); @@ -167,10 +167,9 @@ TEST_P(HazelcastHttpCacheTest, ResponseStaleWithRequestLargeMaxStale) { LookupContextPtr name_lookup_context = lookup(request_path); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - Http::TestResponseHeaderMapImpl response_headers{ - {"date", formatter_.fromTime(current_time_)}, - {"age", "7200"}, - {"cache-control", "public, max-age=3600"}}; + Http::TestResponseHeaderMapImpl response_headers{{"date", formatter_.fromTime(current_time_)}, + {"age", "7200"}, + {"cache-control", "public, max-age=3600"}}; const std::string Body("content"); insert(move(name_lookup_context), response_headers, Body); @@ -178,11 +177,10 @@ TEST_P(HazelcastHttpCacheTest, ResponseStaleWithRequestLargeMaxStale) { } TEST_P(HazelcastHttpCacheTest, StreamingPutAndRangeGet) { - InsertContextPtr inserter = makeInsertContext("/streaming/put"); + InsertContextPtr inserter = cache_->base().makeInsertContext(lookup("/streaming/put")); inserter->insertHeaders(getResponseHeaders(), false); inserter->insertBody( - Buffer::OwnedImpl("Hello, "), - [](bool ready){EXPECT_TRUE(ready); }, false); + Buffer::OwnedImpl("Hello, "), [](bool ready) { EXPECT_TRUE(ready); }, false); inserter->insertBody(Buffer::OwnedImpl("World!"), nullptr, true); LookupContextPtr name_lookup_context = lookup("/streaming/put"); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); @@ -194,18 +192,21 @@ TEST_P(HazelcastHttpCacheTest, StreamingPutAndRangeGet) { TEST(Registration, GetFactory) { HttpCacheFactory* factory = Registry::FactoryRegistry::getFactoryByType( - "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); + "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); ASSERT_NE(factory, nullptr); envoy::extensions::filters::http::cache::v3alpha::CacheConfig config; HazelcastHttpCacheConfig hz_cache_config = HazelcastTestUtil::getTestConfig(true); + hz_cache_config.set_group_name("do-not-connect-any-cluster"); + hz_cache_config.set_connection_attempt_limit(1); + hz_cache_config.set_connection_attempt_period(1); // give up immediately. config.mutable_typed_config()->PackFrom(hz_cache_config); - HazelcastHttpCache& cache = static_cast(factory->getCache(config)); - EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); - // Explicitly destroy Hazelcast connection here. Otherwise the test - // environment does not wait for cache destructor to be completed - // and this causes segfault when Hazelcast Client is shutting down. - cache.shutdown(true); + // getOfflineCache() call is for testing. It creates a HazelcastHttpCache but does + // not make it operational until a connect() call. + HttpCache& cache = static_cast(factory)->getOfflineCache(config); + EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); + EXPECT_THROW_WITH_MESSAGE(static_cast(cache).connect(), EnvoyException, + "Hazelcast Client could not connect to any cluster."); } } // namespace HazelcastHttpCache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 13e50941355e4..517311dbc8161 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -1,6 +1,7 @@ -#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -14,30 +15,35 @@ class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { protected: void SetUp() { HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(false); - cache_ = new HazelcastHttpCache(cfg); - cache_->connect(); - clearMaps(); + // To test the cache with a real Hazelcast instance, remote cache + // must be used during tests. + // cache_ = std::make_unique(cfg); + cache_ = std::make_unique(cfg); + cache_->restoreTestConnection(); + cache_->clearTestMaps(); } }; TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { const std::string RequestPath("/abort/when/max/size/reached"); - InsertContextPtr insert_context = makeInsertContext(RequestPath); + InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup(RequestPath)); insert_context->insertHeaders(getResponseHeaders(), false); bool ready_for_next = true; while (ready_for_next) { insert_context->insertBody( - Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), - [&](bool ready){ready_for_next = ready;}, false); + Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), + [&](bool ready) { ready_for_next = ready; }, false); } EXPECT_EQ( - (HazelcastTestUtil::TEST_MAX_BODY_SIZE / HazelcastTestUtil::TEST_PARTITION_SIZE) + - ((HazelcastTestUtil::TEST_MAX_BODY_SIZE % HazelcastTestUtil::TEST_PARTITION_SIZE) == 0 ? - 0 : 1), testBodyMap().size()); - - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); + (HazelcastTestUtil::TEST_MAX_BODY_SIZE / HazelcastTestUtil::TEST_PARTITION_SIZE) + + ((HazelcastTestUtil::TEST_MAX_BODY_SIZE % HazelcastTestUtil::TEST_PARTITION_SIZE) == 0 + ? 0 + : 1), + cache_->bodyTestMapSize()); + + EXPECT_TRUE(expectLookupSuccessWithBody( + lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); } TEST_F(HazelcastDividedCacheTest, PreventOverridingCacheEntries) { @@ -53,10 +59,9 @@ TEST_F(HazelcastDividedCacheTest, PreventOverridingCacheEntries) { // A possible call to insertion below is filter's fault, not an expected behavior. const std::string OverriddenBody(HazelcastTestUtil::TEST_PARTITION_SIZE * 3, 'z'); insert(move(lookup_context), getResponseHeaders(), OverriddenBody); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), OriginalBody)); - EXPECT_EQ(2, testBodyMap().size()); - EXPECT_EQ(1, testHeaderMap().size()); + EXPECT_EQ(2, cache_->bodyTestMapSize()); + EXPECT_EQ(1, cache_->headerTestMapSize()); } TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { @@ -99,32 +104,29 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*lookup_context).variantHashKey(); + static_cast(*lookup_context).variantHashKey(); const std::string Body(HazelcastTestUtil::TEST_PARTITION_SIZE * 2, 'h'); insert(move(lookup_context), getResponseHeaders(), Body); EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath1).get(), Body)); // Change version of the second partition. - const std::string body2key = getBodyKey(variant_hash_key,1); - auto body2 = testBodyMap().get(body2key); + auto body2 = cache_->base().getBody(variant_hash_key, 1); EXPECT_NE(body2, nullptr); body2->version(body2->version() + 1); - testBodyMap().put(body2key, *body2); + cache_->base().putBody(variant_hash_key, 1, *body2); // Change happened in the second partition. Lookup to the first one should be successful. lookup_context = lookup(RequestPath1); - std::string partition1 = getBody(*lookup_context, 0, - HazelcastTestUtil::TEST_PARTITION_SIZE); + std::string partition1 = getBody(*lookup_context, 0, HazelcastTestUtil::TEST_PARTITION_SIZE); EXPECT_EQ(partition1, std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')); - std::string fullBody = getBody(*lookup_context, 0, - HazelcastTestUtil::TEST_PARTITION_SIZE * 2); + std::string fullBody = getBody(*lookup_context, 0, HazelcastTestUtil::TEST_PARTITION_SIZE * 2); EXPECT_EQ(fullBody, HazelcastTestUtil::abortedBodyResponse()); // Clean up must be performed for malformed entries. - EXPECT_EQ(0, testBodyMap().size()); - EXPECT_EQ(0, testHeaderMap().size()); + EXPECT_EQ(0, cache_->bodyTestMapSize()); + EXPECT_EQ(0, cache_->headerTestMapSize()); } TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { @@ -135,7 +137,7 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*lookup_context).variantHashKey(); + static_cast(*lookup_context).variantHashKey(); const std::string Body("hazelcast"); insert(move(lookup_context), getResponseHeaders(), Body); @@ -143,12 +145,12 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. - auto header = testHeaderMap().get(mapKey(variant_hash_key)); + auto header = cache_->base().getHeader(variant_hash_key); Key modified = header->variantKey(); modified.add_custom_fields("custom1"); modified.add_custom_fields("custom2"); header->variantKey(std::move(modified)); - testHeaderMap().put(mapKey(variant_hash_key), *header); + cache_->base().putHeader(variant_hash_key, *header); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -159,7 +161,7 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - EXPECT_EQ(1, testHeaderMap().size()); + EXPECT_EQ(1, cache_->headerTestMapSize()); } TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { @@ -167,11 +169,11 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { LookupContextPtr lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*lookup_context1).variantHashKey(); + static_cast(*lookup_context1).variantHashKey(); const std::string Body = std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h') + - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z') + - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'); + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z') + + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'); insert(move(lookup_context1), getResponseHeaders(), Body); lookup_context1 = lookup(RequestPath1); @@ -183,19 +185,19 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { // variant_hash_key "2" -> Body3 (in body map) EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); - removeBody(variant_hash_key, 1); // evict Body2. + cache_->removeTestBody(variant_hash_key, 1); // evict Body2. lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); // Lookup for Body1 is OK. lookup_context1->getBody({0, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, - [](Buffer::InstancePtr&& data) {EXPECT_NE(data, nullptr);}); + [](Buffer::InstancePtr&& data) { EXPECT_NE(data, nullptr); }); // Lookup for Body2 must fail and trigger clean up. lookup_context1->getBody( - {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, - [](Buffer::InstancePtr&& data){EXPECT_EQ(data, nullptr);}); + {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, + [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -205,11 +207,11 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { // If not released, the second run for the test fails. Since no // insertion follows the missed lookup here, the lock is explicitly // unlocked. - unlockKey(variant_hash_key); + cache_->base().unlock(variant_hash_key); // Assert clean up - EXPECT_EQ(0, testBodyMap().size()); - EXPECT_EQ(0, testHeaderMap().size()); + EXPECT_EQ(0, cache_->bodyTestMapSize()); + EXPECT_EQ(0, cache_->headerTestMapSize()); } TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { @@ -229,7 +231,7 @@ TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { // then empty body for body insertion. headerOnlyTest("/empty/body/response", true); - EXPECT_EQ(0, testBodyMap().size()); + EXPECT_EQ(0, cache_->bodyTestMapSize()); } TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { @@ -243,13 +245,13 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { lookup_context = lookup(RequestPath); EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); - dropConnection(); + cache_->dropTestConnection(); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); insert(move(lookup_context), getResponseHeaders(), Body); - restoreConnection(); + cache_->restoreTestConnection(); lookup_context = lookup(RequestPath); EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); @@ -257,21 +259,24 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { { const std::string RequestPath("/connection/lost/during/body/insert"); - InsertContextPtr insert_context = makeInsertContext(RequestPath); + InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup(RequestPath)); insert_context->insertHeaders(getResponseHeaders(), false); - insert_context->insertBody(Buffer::OwnedImpl( - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), [](bool) {}, false); - insert_context->insertBody(Buffer::OwnedImpl( - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z')), [](bool) {}, false); + insert_context->insertBody( + Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), [](bool) {}, + false); + insert_context->insertBody( + Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z')), [](bool) {}, + false); - dropConnection(); + cache_->dropTestConnection(); - insert_context->insertBody(Buffer::OwnedImpl( - std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c')), [](bool) {}, false); + insert_context->insertBody( + Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c')), [](bool) {}, + false); LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - restoreConnection(); + cache_->restoreTestConnection(); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc new file mode 100644 index 0000000000000..1c2125449f334 --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc @@ -0,0 +1,187 @@ +#include "test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h" + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +HazelcastTestableLocalCache::HazelcastTestableLocalCache(HazelcastHttpCacheConfig config) + : HazelcastTestableHttpCache(dynamic_cast(*this)), + HazelcastCache(config.unified(), config.body_partition_size(), config.max_body_size()) {} + +void HazelcastTestableLocalCache::clearTestMaps() { + headerMap.clear(); + bodyMap.clear(); + responseMap.clear(); +} + +void HazelcastTestableLocalCache::dropTestConnection() { connected_ = false; } + +void HazelcastTestableLocalCache::restoreTestConnection() { connected_ = true; } + +int HazelcastTestableLocalCache::headerTestMapSize() { + ASSERT(!unified_); + return headerMap.size(); +} + +int HazelcastTestableLocalCache::bodyTestMapSize() { + ASSERT(!unified_); + return bodyMap.size(); +} + +int HazelcastTestableLocalCache::responseTestMapSize() { + ASSERT(unified_); + return responseMap.size(); +} + +void HazelcastTestableLocalCache::putTestResponse(uint64_t key, + const HazelcastResponseEntry& entry) { + checkConnection(); + responseMap[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); +} + +void HazelcastTestableLocalCache::removeTestBody(uint64_t key, uint64_t order) { + checkConnection(); + bodyMap.erase(orderedMapKey(key, order)); +} + +LookupContextPtr HazelcastTestableLocalCache::makeLookupContext(LookupRequest&& request) { + if (unified_) { + return std::make_unique(*this, std::move(request)); + } else { + return std::make_unique(*this, std::move(request)); + } +} + +InsertContextPtr HazelcastTestableLocalCache::makeInsertContext(LookupContextPtr&& lookup_context) { + ASSERT(lookup_context != nullptr); + if (unified_) { + return std::make_unique(*lookup_context, *this); + } else { + return std::make_unique(*lookup_context, *this); + } +} + +void HazelcastTestableLocalCache::updateHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) { + // Not implemented and tested yet. + ASSERT(lookup_context); + ASSERT(response_headers); +} + +CacheInfo HazelcastTestableLocalCache::cacheInfo() const { return CacheInfo(); } + +void HazelcastTestableLocalCache::putHeader(const uint64_t& key, + const HazelcastHeaderEntry& entry) { + checkConnection(); + headerMap[mapKey(key)] = HazelcastHeaderPtr(new HazelcastHeaderEntry(entry)); +} + +void HazelcastTestableLocalCache::putBody(const uint64_t& key, const uint64_t& order, + const HazelcastBodyEntry& entry) { + checkConnection(); + bodyMap[orderedMapKey(key, order)] = HazelcastBodyPtr(new HazelcastBodyEntry(entry)); +} + +HazelcastHeaderPtr HazelcastTestableLocalCache::getHeader(const uint64_t& key) { + checkConnection(); + auto result = headerMap.find(mapKey(key)); + if (result != headerMap.end()) { + // New objects are created during deserialization. Hence not returning the original one here. + return HazelcastHeaderPtr(new HazelcastHeaderEntry(*result->second)); + } else { + return nullptr; + } +} + +HazelcastBodyPtr HazelcastTestableLocalCache::getBody(const uint64_t& key, const uint64_t& order) { + checkConnection(); + auto result = bodyMap.find(orderedMapKey(key, order)); + if (result != bodyMap.end()) { + return HazelcastBodyPtr(new HazelcastBodyEntry(*result->second)); + } else { + return nullptr; + } +} + +void HazelcastTestableLocalCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { + if (!tryLock(key)) { + return; + } + auto header = getHeader(key); + if (header && header->version() != version) { + unlock(key); + return; + } + int body_count = body_size / body_partition_size_; + while (body_count >= 0) { + bodyMap.erase(orderedMapKey(key, body_count--)); + } + headerMap.erase(mapKey(key)); + unlock(key); +} +void HazelcastTestableLocalCache::onVersionMismatch(uint64_t key, int32_t version, + uint64_t body_size) { + onMissingBody(key, version, body_size); +} + +void HazelcastTestableLocalCache::putResponseIfAbsent(const uint64_t& key, + const HazelcastResponseEntry& entry) { + checkConnection(); + if (responseMap.find(mapKey(key)) != responseMap.end()) { + return; + } + responseMap[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); +} + +HazelcastResponsePtr HazelcastTestableLocalCache::getResponse(const uint64_t& key) { + checkConnection(); + auto result = responseMap.find(mapKey(key)); + if (result != responseMap.end()) { + return HazelcastResponsePtr(new HazelcastResponseEntry(*result->second)); + } else { + return nullptr; + } +} + +bool HazelcastTestableLocalCache::tryLock(const uint64_t& key) { + checkConnection(); + if (unified_) { + bool locked = std::find(responseLocks.begin(), responseLocks.end(), key) != responseLocks.end(); + if (locked) { + return false; + } else { + responseLocks.push_back(key); + return true; + } + } else { + bool locked = std::find(headerLocks.begin(), headerLocks.end(), key) != headerLocks.end(); + if (locked) { + return false; + } else { + headerLocks.push_back(key); + return true; + } + } +} + +void HazelcastTestableLocalCache::unlock(const uint64_t& key) { + checkConnection(); + if (unified_) { + responseLocks.erase(std::remove(responseLocks.begin(), responseLocks.end(), key), + responseLocks.end()); + } else { + headerLocks.erase(std::remove(headerLocks.begin(), headerLocks.end(), key), headerLocks.end()); + } +} + +uint64_t HazelcastTestableLocalCache::random() { return std::rand(); } + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h new file mode 100644 index 0000000000000..9827b625d2765 --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h @@ -0,0 +1,140 @@ +#pragma once + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +/** + * Test interface for cache. + */ +class HazelcastTestableHttpCache { +public: + HazelcastTestableHttpCache(HazelcastCache& cache) : cache_(cache){}; + + virtual void clearTestMaps() PURE; + virtual void dropTestConnection() PURE; + virtual void restoreTestConnection() PURE; + + virtual int headerTestMapSize() PURE; + virtual int bodyTestMapSize() PURE; + virtual int responseTestMapSize() PURE; + + virtual void putTestResponse(uint64_t key, const HazelcastResponseEntry& entry) PURE; + virtual void removeTestBody(uint64_t key, uint64_t order) PURE; + + virtual ~HazelcastTestableHttpCache() = default; + + HazelcastCache& base() { return cache_; } + +private: + HazelcastCache& cache_; +}; + +/** + * Testable cache with local storage. + * + * Does not connect to a Hazelcast cluster but instead stores entries locally and + * mimics the remote cache behavior. Running a Hazelcast cluster or a single member + * is not needed during test. + */ +class HazelcastTestableLocalCache : public HazelcastTestableHttpCache, public HazelcastCache { +public: + HazelcastTestableLocalCache(HazelcastHttpCacheConfig config); + + // HazelcastTestableHttpCache + void clearTestMaps() override; + void dropTestConnection() override; + void restoreTestConnection() override; + int headerTestMapSize() override; + int bodyTestMapSize() override; + int responseTestMapSize() override; + void putTestResponse(uint64_t key, const HazelcastResponseEntry& entry) override; + void removeTestBody(uint64_t key, uint64_t order) override; + + // HttpCache + LookupContextPtr makeLookupContext(LookupRequest&& request) override; + InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; + void updateHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) override; + CacheInfo cacheInfo() const override; + + // HazelcastCache + void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) override; + void putBody(const uint64_t& key, const uint64_t& order, + const HazelcastBodyEntry& entry) override; + HazelcastHeaderPtr getHeader(const uint64_t& key) override; + HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order) override; + void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) override; + void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) override; + void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry) override; + HazelcastResponsePtr getResponse(const uint64_t& key) override; + bool tryLock(const uint64_t& key) override; + void unlock(const uint64_t& key) override; + uint64_t random() override; + +private: + void checkConnection() { + if (!connected_) { + throw std::exception(); + } + } + + std::unordered_map headerMap; + std::unordered_map bodyMap; + std::unordered_map responseMap; + + std::vector headerLocks; + std::vector responseLocks; + + bool connected_ = false; +}; + +/** + * Test wrapper for HazelcastHttpCache. + * + * Establishes a TCP connection with an alive Hazelcast member during tests and + * stores data in distributed map. Simulates the original version of the cache. + */ +class HazelcastTestableRemoteCache : public HazelcastTestableHttpCache, public HazelcastHttpCache { +public: + HazelcastTestableRemoteCache(HazelcastHttpCacheConfig config) + : HazelcastTestableHttpCache(dynamic_cast(*this)), + HazelcastHttpCache(config) {} + + void clearTestMaps() override { + if (unified_) { + getResponseMap().clear(); + } else { + getBodyMap().clear(); + getHeaderMap().clear(); + } + } + + void dropTestConnection() override { shutdown(false); } + + void restoreTestConnection() override { connect(); } + + int headerTestMapSize() override { return getHeaderMap().size(); } + + int bodyTestMapSize() override { return getBodyMap().size(); } + + int responseTestMapSize() override { return getResponseMap().size(); } + + void removeTestBody(uint64_t key, uint64_t order) override { + getBodyMap().remove(orderedMapKey(key, order)); + } + + void putTestResponse(uint64_t key, const HazelcastResponseEntry& entry) override { + getResponseMap().put(mapKey(key), entry); + } +}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 09785769b21f7..778bf90dc9540 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -1,10 +1,9 @@ -#include "test/test_common/simulated_time_system.h" -#include "test/test_common/utility.h" -#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" - #include "envoy/registry/registry.h" + #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -17,25 +16,28 @@ namespace HazelcastHttpCache { class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { void SetUp() { HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(true); - cache_ = new HazelcastHttpCache(cfg); - cache_->connect(); - clearMaps(); + // To test the cache with a real Hazelcast instance, remote cache + // must be used during tests. + // cache_ = std::make_unique(cfg); + cache_ = std::make_unique(cfg); + cache_->restoreTestConnection(); + cache_->clearTestMaps(); } }; TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedInsertionWhenMaxSizeReached) { const std::string RequestPath("/abort/when/max/size/reached"); - InsertContextPtr insert_context = makeInsertContext(RequestPath); + InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup(RequestPath)); insert_context->insertHeaders(getResponseHeaders(), false); bool ready_for_next = true; while (ready_for_next) { insert_context->insertBody( - Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE / 3, 'h')), - [&](bool ready){ready_for_next = ready;}, false); + Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE / 3, 'h')), + [&](bool ready) { ready_for_next = ready; }, false); } EXPECT_TRUE(expectLookupSuccessWithBody( - lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); + lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); } TEST_F(HazelcastUnifiedCacheTest, PutResponseOnlyWhenAbsent) { @@ -79,7 +81,7 @@ TEST_F(HazelcastUnifiedCacheTest, DoNotOverrideExistingResponse) { } TEST_F(HazelcastUnifiedCacheTest, UnifiedHeaderOnlyResponse) { - InsertContextPtr insert_context = makeInsertContext("/header/only"); + InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup("/header/only")); insert_context->insertHeaders(getResponseHeaders(), true); LookupContextPtr lookup_context = lookup("/header/only"); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); @@ -94,7 +96,7 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); uint64_t variant_hash_key = - static_cast(*lookup_context).variantHashKey(); + static_cast(*lookup_context).variantHashKey(); const std::string Body("hazelcast"); insert(move(lookup_context), getResponseHeaders(), Body); @@ -103,12 +105,12 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. - auto response = testResponseMap().get(mapKey(variant_hash_key)); + auto response = cache_->base().getResponse(variant_hash_key); Key modified = response->header().variantKey(); modified.add_custom_fields("custom1"); modified.add_custom_fields("custom2"); response->header().variantKey(std::move(modified)); - testResponseMap().put(mapKey(variant_hash_key), *response); + cache_->putTestResponse(variant_hash_key, *response); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -119,7 +121,7 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - EXPECT_EQ(1, testResponseMap().size()); + EXPECT_EQ(1, cache_->responseTestMapSize()); } TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { @@ -132,13 +134,13 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { lookup_context1 = lookup(RequestPath1); EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); - dropConnection(); + cache_->dropTestConnection(); lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); insert(move(lookup_context1), getResponseHeaders(), Body); - restoreConnection(); + cache_->restoreTestConnection(); lookup_context1 = lookup(RequestPath1); EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h index 1ff6f7a6b36ce..e0ebb4eb53c8d 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -1,10 +1,11 @@ #pragma once -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" -#include "gtest/gtest.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h" #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" +#include "gtest/gtest.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -13,16 +14,15 @@ namespace HazelcastHttpCache { class HazelcastTestUtil { public: - static constexpr int TEST_PARTITION_SIZE = 10; static constexpr int TEST_MAX_BODY_SIZE = TEST_PARTITION_SIZE * 20; - static Runtime::RandomGeneratorImpl& randomGenerator(){ + static Runtime::RandomGeneratorImpl& randomGenerator() { static Runtime::RandomGeneratorImpl rand; return rand; } - static const std::string& abortedBodyResponse(){ + static const std::string& abortedBodyResponse() { static std::string response("NULL_BODY"); return response; } @@ -50,22 +50,18 @@ class HazelcastTestUtil { headers.setForwardedProto("https"); headers.setCacheControl("max-age=3600"); } - }; /** - * The base environment for DIVIDED and UNIFIED cache mode tests. + * The base test environment for DIVIDED and UNIFIED cache mode tests. * - * A similar test environment to SimpleHttpCacheTest is applied and + * A similar environment to SimpleHttpCacheTest is applied and * some functions & fields are derived directly. * */ class HazelcastHttpCacheTestBase : public testing::Test { protected: - - HazelcastHttpCacheTestBase() { - HazelcastTestUtil::setRequestHeaders(request_headers_); - } + HazelcastHttpCacheTestBase() { HazelcastTestUtil::setRequestHeaders(request_headers_); } // Makes getBody requests until requested range is satisfied. // Returns the body on success; HazelcastTestUtil::abortedBodyResponse() on @@ -79,101 +75,46 @@ class HazelcastHttpCacheTestBase : public testing::Test { return HazelcastTestUtil::abortedBodyResponse(); } AdjustedByteRange range(offset, end); - context.getBody(range, [&aborted, &body_chunk, &offset, - &full_body](Buffer::InstancePtr&& data) { - if (data) { - body_chunk = data->toString(); - full_body.append(body_chunk); - offset += body_chunk.length(); - } else { - aborted = true; - } - }); + context.getBody(range, + [&aborted, &body_chunk, &offset, &full_body](Buffer::InstancePtr&& data) { + if (data) { + body_chunk = data->toString(); + full_body.append(body_chunk); + offset += body_chunk.length(); + } else { + aborted = true; + } + }); } return full_body; } Http::TestResponseHeaderMapImpl getResponseHeaders() { - return Http::TestResponseHeaderMapImpl{ - {"date", formatter_.fromTime(current_time_)}, - {"cache-control", "public, max-age=3600"}}; - } - - /// Test environments make cache calls over these functions. - - void unlockKey(uint64_t key) { - cache_->unlock(key); - } - - InsertContextPtr makeInsertContext(absl::string_view path) { - return cache_->makeInsertContext(lookup(path)); - } - - void clearMaps() { - if (cache_->unified_) { - cache_->getResponseMap().clear(); - } else { - cache_->getBodyMap().clear(); - cache_->getHeaderMap().clear(); - } - } - - void removeBody(uint64_t key, uint64_t order) { - ASSERT(!cache_->unified_); - cache_->getBodyMap().remove(cache_->orderedMapKey(key, order)); - } - - IMap testHeaderMap() { - ASSERT(!cache_->unified_); - return cache_->getHeaderMap(); - }; - - IMap testBodyMap() { - ASSERT(!cache_->unified_); - return cache_->getBodyMap(); - }; - - IMap testResponseMap() { - ASSERT(cache_->unified_); - return cache_->getResponseMap(); - }; - - std::string getBodyKey(uint64_t key, uint64_t order){ - return cache_->orderedMapKey(key, order); - } - - int64_t mapKey(uint64_t key){ - return cache_->mapKey(key); - } - - void dropConnection(){ - cache_->shutdown(false); - } - - void restoreConnection() { - cache_->connect(); + return Http::TestResponseHeaderMapImpl{{"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public, max-age=3600"}}; } /// from SimpleHttpCacheTest LookupContextPtr lookup(absl::string_view request_path) { LookupRequest request = makeLookupRequest(request_path); - LookupContextPtr context = cache_->makeLookupContext(std::move(request)); - context->getHeaders([this](LookupResult&& result) {lookup_result_ = std::move(result); }); + LookupContextPtr context = cache_->base().makeLookupContext(std::move(request)); + context->getHeaders([this](LookupResult&& result) { lookup_result_ = std::move(result); }); return context; } void insert(LookupContextPtr lookup, const Http::TestResponseHeaderMapImpl& response_headers, - const absl::string_view response_body) { - InsertContextPtr insert_context = cache_->makeInsertContext(move(lookup)); + const absl::string_view response_body) { + InsertContextPtr insert_context = cache_->base().makeInsertContext(move(lookup)); insert_context->insertHeaders(response_headers, response_body == nullptr); - if (response_body == nullptr) return; + if (response_body == nullptr) + return; insert_context->insertBody(Buffer::OwnedImpl(response_body), nullptr, true); } void insert(absl::string_view request_path, - const Http::TestResponseHeaderMapImpl& response_headers, - const absl::string_view response_body) { + const Http::TestResponseHeaderMapImpl& response_headers, + const absl::string_view response_body) { insert(lookup(request_path), response_headers, response_body); } @@ -203,7 +144,7 @@ class HazelcastHttpCacheTestBase : public testing::Test { return AssertionSuccess(); } - HazelcastHttpCache* cache_; + std::unique_ptr cache_; LookupResult lookup_result_; Http::TestRequestHeaderMapImpl request_headers_; Event::SimulatedTimeSystem time_source_; From 22327616fa0957f3c0c11c9bfd93c766193cab0b Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Sun, 5 Apr 2020 21:43:50 +0300 Subject: [PATCH 06/33] Add plugin doc and fix spell check. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_cache_plugin.md | 96 +++++++++++++++++++ .../cache/hazelcast_http_cache/config.proto | 2 +- .../hazelcast_http_cache/hazelcast_context.cc | 3 +- .../hazelcast_http_cache_impl.cc | 3 +- .../http/cache/hazelcast_http_cache/util.h | 7 +- .../http/cache/hazelcast_http_cache/BUILD | 9 ++ .../hazelcast_http_cache/config_utils_test.cc | 88 +++++++++++++++++ .../hazelcast_common_cache_test.cc | 6 +- .../hazelcast_divided_cache_test.cc | 8 +- .../hazelcast_test_cache.cc | 4 +- .../hazelcast_unified_cache_test.cc | 8 +- tools/spelling/spelling_dictionary.txt | 1 + 12 files changed, 215 insertions(+), 20 deletions(-) create mode 100644 source/docs/filters/http/cache/hazelcast_cache_plugin.md create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc diff --git a/source/docs/filters/http/cache/hazelcast_cache_plugin.md b/source/docs/filters/http/cache/hazelcast_cache_plugin.md new file mode 100644 index 0000000000000..a48655891fa8e --- /dev/null +++ b/source/docs/filters/http/cache/hazelcast_cache_plugin.md @@ -0,0 +1,96 @@ +# Hazelcast Http Cache Plugin +Hazelcast Http Cache provides a pluggable storage implementation backed by Hazelcast In Memory Data Grid for the http +cache filter. Using Hazelcast C++ client, the plugin does not store any http response locally but in a distributed map +provided by Hazelcast cluster. To enable the cache plugin, the network configuration belongs to a cluster to be +connected must be set on the cache plugin configuration. + +## Offered cache modes +The plugin comes with two modes: + + - **Unified** +A cached http response is stored as a single entry in the cache. On a range http request, regardless of the requested +range, the whole response body is fetched from the cache and then only the desired bytes are served along with the +headers and trailers (if any). This mode is handy where response body sizes are reasonably large (up to 10 KB), or +range requests are not frequent, or they are not allowed at all. + + - **Divided** +A cached http response is stored as multiple entries in the cache. Two separate maps are used to store a single +response. In one of them, response headers, body size, and trailers (if any) are stored. In the other one, the +corresponding response body is stored in multiple entries each of which has a certain size configured via `partition +size` in the plugin configuration. That is, for a response of size 50 KB, if the configured partition size is 20 KB, +then three different entries will be created to store the body of this response: + - Body<1> : 0 - 19 KB + - Body<2> : 20 - 39 KB + - Body<3> : 40 - 50 KB + + On a range request, not the whole body for a response but only the necessary partitions are fetched from the + cache. This option helps to serve range requests faster and in a stream-like fashion but comes with a cost. Every + body entry has its own fixed memory cost and hence partitioned entries need larger memory than the actual body + size. Also, to keep these partitioned cache entries even, extra operations - not necessarily asynchronous, might + be needed (i.e. cleaning up a malformed body sequence, recovery from a mismatch between body and header, etc.). + +## Connecting to a Hazelcast cluster +**NOTE:** The plugin uses the client with version 3.12.1 and hence it is not yet compatible with Hazelcast 4.x. +Hazelcast version 3.12.x is recommended for the server-side. + +Before starting the cache plugin, there must be a running Hazelcast cluster. Hazelcast instances might be started +as a sidecar to Envoy, form up a cluster using Hazelcast Kubernetes plugin, etc. The only information the plugin needs +will be the addresses and ports of the cluster members and the group name of the cluster. Providing the address of only +one member in the cluster will be enough for the connection but using more than one is recommended. + +Related links: [Hazelcast Docker](https://hub.docker.com/r/hazelcast/hazelcast/), [Hazelcast Kubernetes Plugin] +(https://github.com/hazelcast/hazelcast-kubernetes) + +## Configuring Hazelcast cluster for the cache +Eviction, maximum size, and other related properties for the cache must be configured on the server-side. That is, +in `hazelcast.xml` before starting the cluster: +```xml + + + + + 1000 + LFU + 25 + 180 + + + BINARY + true + +``` +When one of the clients connected to the cluster loses its connection, if the client has acquired the lock for a +key, this will cause this key to be unusable. To prevent such a scenario in a possible connection failure, the +maximum time limit for the locks should be set on the server-side (not necessarily to be 60 seconds): +```xml + + ... + 60 + ... + +``` +**NOTE**: Setting this property will affect not only the http cache but all other data structures in the cluster. + +## Statistics +Cache statistics are not collected locally. Instead, cluster-wide statistics should be observed on Hazelcast +Management Center. When the cache plugin starts, one of the very first logs will be saying the map name used for +the cache. The statistics can be observed with that name under the `maps` section on the management center. + +## Using a single cache for multiple filters +Each distributed map in a Hazelcast cluster is differentiated by its name for the same key and value types. Thus, +all the plugins connected to the same cluster will use the same map for responses only if they have the same cache +mode and the app prefix (and the same partition size for divided mode) in the plugin configuration. The filters +configured with the same partition size and cache mode but different prefixes will create two different http caches. \ No newline at end of file diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto index 31f91c0ad9557..621614284bcce 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package envoy.source.extensions.filters.http.cache; // [#protodoc-title: HazelcastHttpCache CacheFilter storage plugin] -// CacheFilter plugin backed by Hazelcast IMDG. +// CacheFilter plugin backed by Hazelcast In Memory Data Grid. // [#extension: envoy.extensions.http.cache] // Hazelcast Http Cache configuration diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index ab32f491bc587..9d9150614785a 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -1,7 +1,6 @@ -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" - #include "common/buffer/buffer_impl.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" #include "extensions/filters/http/cache/hazelcast_http_cache/util.h" namespace Envoy { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc index 6e953af6ba99c..66aa62ebb4be0 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc @@ -1,6 +1,5 @@ -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" - #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" #include "extensions/filters/http/cache/hazelcast_http_cache/util.h" namespace Envoy { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h index a4f1b3b38c96f..8a218fd583ca7 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -28,6 +28,7 @@ class ConfigUtil { cache_config) { hazelcast::client::ClientConfig config; config.getGroupConfig().setName(cache_config.group_name()); + config.getGroupConfig().setPassword(cache_config.group_password()); config.getNetworkConfig().setConnectionTimeout(cache_config.connection_timeout() == 0 ? DEFAULT_CONNECTION_TIMEOUT_MS : cache_config.connection_timeout()); @@ -50,12 +51,12 @@ class ConfigUtil { return config; } - static short partitionWarnLimit() { return WARN_PARTITION_LIMIT; } + static uint16_t partitionWarnLimit() { return PARTITION_WARN_LIMIT; } private: // After this much body partitions stored for a response in DIVIDED mode, // a suggestion log will be appeared to increase partition size. - static constexpr short WARN_PARTITION_LIMIT = 16; + static constexpr uint16_t PARTITION_WARN_LIMIT = 16; // Sizes for each divided body entry. static constexpr uint64_t DEFAULT_PARTITION_SIZE = 2048; @@ -79,6 +80,8 @@ class ConfigUtil { // Duration for an invocation to be cancelled. static constexpr uint32_t DEFAULT_INVOCATION_TIMEOUT_SEC = 8; + + friend class ConfigUtilsTest; }; } // namespace HazelcastHttpCache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index ef5d6ef7eeb29..10097f298a832 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -42,6 +42,15 @@ envoy_extension_cc_test( ], ) +envoy_extension_cc_test( + name = "hazelcast_config_utils_test", + srcs = ["config_utils_test.cc"], + extension_name = "envoy.filters.http.cache.hazelcast_http_cache", + deps = [ + ":hazelcast_test_lib", + ], +) + envoy_extension_cc_test_library( name = "hazelcast_test_lib", srcs = ["hazelcast_test_cache.cc"], diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc new file mode 100644 index 0000000000000..badc3767f4982 --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc @@ -0,0 +1,88 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; + +class ConfigUtilsTest : public testing::Test { +protected: + uint64_t defaultPartitionSize() { return ConfigUtil::DEFAULT_PARTITION_SIZE; } + uint64_t maxPartitionSize() { return ConfigUtil::MAX_PARTITION_SIZE; } + uint64_t maxUnifiedBodySize() { return ConfigUtil::MAX_UNIFIED_BODY_SIZE; } + uint64_t maxDividedBodySize() { return ConfigUtil::MAX_DIVIDED_BODY_SIZE; } + uint32_t defaultConnectionTimeoutMs() { return ConfigUtil::DEFAULT_CONNECTION_TIMEOUT_MS; } + uint32_t defaultConnectionAttemptLimit() { return ConfigUtil::DEFAULT_CONNECTION_ATTEMPT_LIMIT; } + uint32_t defaultConnectionAttemptPeriodMs() { + return ConfigUtil::DEFAULT_CONNECTION_ATTEMPT_PERIOD_MS; + } + uint32_t defaultInvocationTimeoutSec() { return ConfigUtil::DEFAULT_INVOCATION_TIMEOUT_SEC; } + uint16_t partitionWarnLimit() { return ConfigUtil::PARTITION_WARN_LIMIT; } +}; + +TEST_F(ConfigUtilsTest, ValidPartitionSizeTest) { + uint64_t valid_value = ConfigUtil::validPartitionSize(0); + EXPECT_EQ(defaultPartitionSize(), valid_value); + valid_value = ConfigUtil::validPartitionSize(maxPartitionSize() + 1); + EXPECT_EQ(maxPartitionSize(), valid_value); + valid_value = ConfigUtil::validPartitionSize(maxPartitionSize() - 1); + EXPECT_EQ(maxPartitionSize() - 1, valid_value); +} + +TEST_F(ConfigUtilsTest, ValidMaxBodySizeTest) { + auto validateBodySizeTest = [this](bool unified) { + uint64_t max_size = unified ? maxUnifiedBodySize() : maxDividedBodySize(); + uint64_t valid_value = ConfigUtil::validMaxBodySize(0, unified); + EXPECT_EQ(max_size, valid_value); + valid_value = ConfigUtil::validMaxBodySize(max_size + 1, unified); + EXPECT_EQ(max_size, valid_value); + valid_value = ConfigUtil::validMaxBodySize(max_size - 1, unified); + EXPECT_EQ(max_size - 1, valid_value); + }; + validateBodySizeTest(true); + validateBodySizeTest(false); +} + +TEST_F(ConfigUtilsTest, ClientConfigTest) { + const std::string group_name = "group_foo"; + const std::string group_pass = "foo_pass"; + const std::string random_ip = "192.168.10.3"; + constexpr int random_port = 5703; + + HazelcastHttpCacheConfig default_cache_config; + default_cache_config.set_group_name(group_name); + default_cache_config.set_group_password(group_pass); + HazelcastHttpCacheConfig::MemberAddress* memberAddress = default_cache_config.add_addresses(); + memberAddress->set_ip(random_ip); + memberAddress->set_port(random_port); + + hazelcast::client::ClientConfig config = ConfigUtil::getClientConfig(default_cache_config); + + EXPECT_EQ(defaultConnectionTimeoutMs(), config.getNetworkConfig().getConnectionTimeout()); + EXPECT_EQ(defaultConnectionAttemptLimit(), config.getNetworkConfig().getConnectionAttemptLimit()); + EXPECT_EQ(defaultConnectionAttemptPeriodMs(), + config.getNetworkConfig().getConnectionAttemptPeriod()); + EXPECT_STREQ(std::to_string(defaultInvocationTimeoutSec()).c_str(), + config.getProperties()["hazelcast.client.invocation.timeout.seconds"].c_str()); + EXPECT_STREQ(group_name.c_str(), config.getGroupConfig().getName().c_str()); + EXPECT_STREQ(group_pass.c_str(), config.getGroupConfig().getPassword().c_str()); + std::vector addresses = config.getNetworkConfig().getAddresses(); + EXPECT_EQ(1, addresses.size()); + EXPECT_STREQ(random_ip.c_str(), addresses.at(0).getHost().c_str()); + EXPECT_EQ(random_port, addresses.at(0).getPort()); +} + +TEST_F(ConfigUtilsTest, WarnLimitTest) { + EXPECT_EQ(partitionWarnLimit(), ConfigUtil::partitionWarnLimit()); +} + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 2d674c4f46312..47e36e89a558d 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -17,11 +17,11 @@ class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, public testing::WithParamInterface { protected: void SetUp() { - HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(GetParam()); + HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(GetParam()); // To test the cache with a real Hazelcast instance, remote cache // must be used during tests. - // cache_ = std::make_unique(cfg); - cache_ = std::make_unique(cfg); + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); cache_->restoreTestConnection(); cache_->clearTestMaps(); } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 517311dbc8161..424d22dc3d7a1 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -14,11 +14,11 @@ namespace HazelcastHttpCache { class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { protected: void SetUp() { - HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(false); + HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(false); // To test the cache with a real Hazelcast instance, remote cache // must be used during tests. - // cache_ = std::make_unique(cfg); - cache_ = std::make_unique(cfg); + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); cache_->restoreTestConnection(); cache_->clearTestMaps(); } @@ -43,7 +43,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { cache_->bodyTestMapSize()); EXPECT_TRUE(expectLookupSuccessWithBody( - lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); + lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_MAX_BODY_SIZE, 'h'))); } TEST_F(HazelcastDividedCacheTest, PreventOverridingCacheEntries) { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc index 1c2125449f334..d5796a0971bee 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc @@ -1,7 +1,7 @@ -#include "test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h" - #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 778bf90dc9540..89706497abee6 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -15,11 +15,11 @@ namespace HazelcastHttpCache { */ class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { void SetUp() { - HazelcastHttpCacheConfig cfg = HazelcastTestUtil::getTestConfig(true); + HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(true); // To test the cache with a real Hazelcast instance, remote cache // must be used during tests. - // cache_ = std::make_unique(cfg); - cache_ = std::make_unique(cfg); + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); cache_->restoreTestConnection(); cache_->clearTestMaps(); } @@ -37,7 +37,7 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedInsertionWhenMaxSizeReached) { } EXPECT_TRUE(expectLookupSuccessWithBody( - lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'))); + lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_MAX_BODY_SIZE, 'h'))); } TEST_F(HazelcastUnifiedCacheTest, PutResponseOnlyWhenAbsent) { diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index d67bc05863b3a..c07ca2a2bbec7 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -120,6 +120,7 @@ GRPC GTEST GURL Grabbit +HAZELCAST Hashable HC HCM From 8ca71fcd164a3d953d8c4b4b4af5b13abf339af0 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Tue, 7 Apr 2020 20:20:14 +0300 Subject: [PATCH 07/33] Fix code format & clean up. Signed-off-by: Enes Ozcan --- bazel/repository_locations.bzl | 4 +- .../http/cache/hazelcast_cache_plugin.md | 122 +++++++++++++-- .../cache/hazelcast_http_cache/config.proto | 19 ++- .../hazelcast_http_cache/hazelcast_cache.h | 50 +++--- .../hazelcast_cache_entry.h | 10 +- .../hazelcast_http_cache/hazelcast_context.cc | 143 ++++++++++++++---- .../hazelcast_http_cache/hazelcast_context.h | 132 ++++++---------- .../hazelcast_http_cache_impl.cc | 64 +++----- .../hazelcast_http_cache_impl.h | 31 ++-- .../http/cache/hazelcast_http_cache/util.h | 42 +++-- .../http/cache/hazelcast_http_cache/BUILD | 8 +- .../hazelcast_http_cache/config_utils_test.cc | 34 +++-- .../hazelcast_common_cache_test.cc | 23 +-- .../hazelcast_divided_cache_test.cc | 59 ++++---- .../hazelcast_test_cache.cc | 23 ++- .../hazelcast_test_cache.h | 18 +-- .../hazelcast_unified_cache_test.cc | 14 +- .../http/cache/hazelcast_http_cache/util.h | 9 +- 18 files changed, 475 insertions(+), 330 deletions(-) diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index f1652463e35c6..0550737562c6c 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -119,8 +119,8 @@ REPOSITORY_LOCATIONS = dict( sha256 = "3c43c81135e415ce708486564dc125bde93c2c9f8965d5af4b603ec91ff52f6e", strip_prefix = "hazelcast-cpp-client-3.12.1", # Using non official tarball due to missing submodule files in the official release. - # TODO(enozcan): Official tar can be used with a solution to init&update submodules - urls = ["https://github.com/enozcan/envoy/raw/hazelcast_tarball/hazelcast-cpp-client-3.12.1.zip"], + # TODO(enozcan): Use official release with init & updating submodules + urls = ["https://github.com/enozcan/envoy-hazelcast-cpp-client/raw/master/hazelcast-cpp-client-3.12.1.zip"], ), com_github_luajit_luajit = dict( sha256 = "409f7fe570d3c16558e594421c47bdd130238323c9d6fd6c83dedd2aaeb082a8", diff --git a/source/docs/filters/http/cache/hazelcast_cache_plugin.md b/source/docs/filters/http/cache/hazelcast_cache_plugin.md index a48655891fa8e..5e5b30ea3090d 100644 --- a/source/docs/filters/http/cache/hazelcast_cache_plugin.md +++ b/source/docs/filters/http/cache/hazelcast_cache_plugin.md @@ -1,4 +1,6 @@ -# Hazelcast Http Cache Plugin +### Hazelcast Http Cache Plugin +Work in Progress--Cache filter has not implemented features. The corresponding ones are not ready for the plugin too. + Hazelcast Http Cache provides a pluggable storage implementation backed by Hazelcast In Memory Data Grid for the http cache filter. Using Hazelcast C++ client, the plugin does not store any http response locally but in a distributed map provided by Hazelcast cluster. To enable the cache plugin, the network configuration belongs to a cluster to be @@ -35,49 +37,137 @@ Hazelcast version 3.12.x is recommended for the server-side. Before starting the cache plugin, there must be a running Hazelcast cluster. Hazelcast instances might be started as a sidecar to Envoy, form up a cluster using Hazelcast Kubernetes plugin, etc. The only information the plugin needs -will be the addresses and ports of the cluster members and the group name of the cluster. Providing the address of only -one member in the cluster will be enough for the connection but using more than one is recommended. +will be the addresses and ports of the cluster members and the group information of the cluster. Providing the address +of only one member in the cluster will be enough for the connection but using more than one is recommended. -Related links: [Hazelcast Docker](https://hub.docker.com/r/hazelcast/hazelcast/), [Hazelcast Kubernetes Plugin] +Related links: [Hazelcast Docker Hub](https://hub.docker.com/r/hazelcast/hazelcast/), [Hazelcast Kubernetes Plugin] (https://github.com/hazelcast/hazelcast-kubernetes) ## Configuring Hazelcast cluster for the cache -Eviction, maximum size, and other related properties for the cache must be configured on the server-side. That is, -in `hazelcast.xml` before starting the cluster: +Eviction, maximum size, and other related properties for the cache must be configured on the server-side +via programmatic configuration or `hazelcast.xml`. + + - **Unified Mode** + ```xml - - + + + + + --> 1000 - LFU 25 + LRU 180 + 90 + BINARY + true + +``` + + - **Divided Mode** + +```xml + + + + + 100 + 25 + LRU + 180 + + BINARY true + + + + + 0 + 25 + LRU + 195 + + + BINARY + false + ``` + When one of the clients connected to the cluster loses its connection, if the client has acquired the lock for a key, this will cause this key to be unusable. To prevent such a scenario in a possible connection failure, the -maximum time limit for the locks should be set on the server-side (not necessarily to be 60 seconds): +maximum time limit for the locks should be set on the server-side (not necessarily to be 60 seconds). The default +value for this property is `Long.MAX`. Hence, if it is not set, on a connection failure a locked key will +be unusable permanently: ```xml ... + 60 ... diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto index 621614284bcce..defa6be369f99 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -9,14 +9,14 @@ package envoy.source.extensions.filters.http.cache; // Hazelcast Http Cache configuration message HazelcastHttpCacheConfig { - // Address for Hazelcast cluster member. + // Address for Hazelcast cluster member to be connected. message MemberAddress { string ip = 1; int32 port = 2; } // Group name of Hazelcast cluster to be connected. Not only the address of a member - // but its group name must match. + // but its group name and password must match. string group_name = 1; // Group password of Hazelcast cluster to be connected. string group_password = 2; @@ -44,13 +44,13 @@ message HazelcastHttpCacheConfig { uint32 invocation_timeout = 6; // Only one member address is enough to connect to the cluster but - // providing more than one is recommended. - // By default, 127.0.0.1:5701 will be tried. + // providing more than one is recommended. By default, 127.0.0.1:5701 will + // be tried. repeated MemberAddress addresses = 7; // Application specific name for the cache. Different deployments should - // use the same prefix if they want to share the same cache and connect - // to the same Hazelcast cluster. Empty string by default. + // use the same prefix and connect to the same Hazelcast cluster if they want + // to share the same cache. Empty string by default. string app_prefix = 8; // In unified mode, cached responses will be stored as a single entry. @@ -67,12 +67,15 @@ message HazelcastHttpCacheConfig { bool unified = 9; // Body partition size for divided cache. Ignored in unified mode. - // At most 64 KB allowed. 2 KB by default. + // At most 32 KB is allowed. 16 KB by default. uint64 body_partition_size = 10; // Maximum allowed body size per response. If insertion for a larger // value than the limit is attempted, the first max_body_size bytes // of the response will be cached and the remaining will be ignored. - // At most 64 KB for UNIFIED mode, 2 MB for DIVIDED mode. + // At most 32 KB is allowed for UNIFIED mode. In DIVIDED mode, there + // is no such an upper limit but keeping (max_body_size / body_partition_size) + // below 20 is recommended since each partition causes an extra + // network call made to the distributed map. uint64 max_body_size = 11; } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h index 8d574ecc7392c..1c8f2366c250d 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h @@ -14,17 +14,16 @@ namespace HazelcastHttpCache { * * In UNIFIED mode, an HTTP response is wrapped by a HazelcastResponseEntry * with its all fields (headers, body, trailers, request key) and stored in - * distributed map. On a range Http request, regardless of the requested + * distributed map. On a range HTTP request, regardless of the requested * range, the whole response body is fetched from the cache. * * In DIVIDED mode, an HTTP response's fields except for its body are wrapped - * by a HazelcastHeaderEntry. It's body is divided into chunks with a certain - * size and then stored in another distributed map as HazelcastBodyEntry. On + * by a HazelcastHeaderEntry. Its body is divided into chunks with certain + * sizes and then stored in another distributed map as HazelcastBodyEntry. On * a range request, not the whole body for the response but only the necessary * partitions are fetched from the cache. A header and its bodies have a common - * number named to interrelate multiple entries. + * number named to interrelate multiple entries belong to the same response. * - * @note Lookup and Insert contexts should use this base, not HttpCache. */ class HazelcastCache : public HttpCache { public: @@ -42,7 +41,7 @@ class HazelcastCache : public HttpCache { * same Hazelcast cluster might store the same response with different * keys. */ - virtual void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) PURE; + virtual void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) PURE; /** * Puts a body entry into body cache. @@ -51,7 +50,7 @@ class HazelcastCache : public HttpCache { * @param order Order of the body chunk among other partitions * @param entry Entry to be inserted */ - virtual void putBody(const uint64_t& key, const uint64_t& order, + virtual void putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) PURE; /** @@ -59,7 +58,7 @@ class HazelcastCache : public HttpCache { * @param key Hash key for the entry * @return HazelcastHeaderPtr to cached entry if found, nullptr otherwise */ - virtual HazelcastHeaderPtr getHeader(const uint64_t& key) PURE; + virtual HazelcastHeaderPtr getHeader(const uint64_t key) PURE; /** * Performs a lookup to body cache for the given key and order pair. @@ -67,12 +66,13 @@ class HazelcastCache : public HttpCache { * @param order Order of the body chunk among other partitions * @return HazelcastBodyPtr to cached entry if found, nullptr otherwise. */ - virtual HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order) PURE; + virtual HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) PURE; /** * Cleans up a malformed response when at least one of the body chunks are missed - * during lookup. All bodies and the header for the response are removed to make - * a new insertion available by an insert context. + * during lookup. The header for the response is removed to make a new insertion + * available by an insert context and the remaining body partitions are removed + * to prevent orphan body entries stay in the cache. * @param key Header key for the response * @param version Version for the key and body * @param body_size Total body size for the response @@ -94,42 +94,42 @@ class HazelcastCache : public HttpCache { * Puts a unified entry into unified cache if no other entry associated with the key * is found. * @note IfAbsent is to prevent race between multiple filters. Overriding - * an existing entry is forbidden. HttpCache::updateHeaders() might - * be used when necessary. + * an existing entry is forbidden. HttpCache::updateHeaders() should + * be used if changing the header content is necessary. * @param key Hash key for the entry * @param entry Entry to be inserted */ - virtual void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry) PURE; + virtual void putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) PURE; /** * Performs a lookup to unified cache for the given key. * @param key Hash key for the entry. * @return HazelcastResponsePtr to cached entry if found, nullptr otherwise. */ - virtual HazelcastResponsePtr getResponse(const uint64_t& key) PURE; + virtual HazelcastResponsePtr getResponse(const uint64_t key) PURE; /// Common /** * Attempts to lock the given key in the cache. When a key is locked, a lookup * can be performed but an insertion or update for the key must be prevented. - * @note Used to prevent multiple insertions or updates from different - * filters at a time. + * @note Used to prevent multiple insertions or updates by different + * contexts at a time. * @param key Key to be locked. * @return True if acquired, false otherwise. */ - virtual bool tryLock(const uint64_t& key) PURE; + virtual bool tryLock(const uint64_t key) PURE; /** * Releases the lock for the key. * @param Key to be unlocked */ - virtual void unlock(const uint64_t& key) PURE; + virtual void unlock(const uint64_t key) PURE; /** - * Produces a random number which is not necessarily perfect uniform or random. + * Produces a random number. * @return Random unsigned long. - * @note Primary use cases for the random number is to generate version + * @note The primary use case for the random number is to generate version * for header and body entries. */ virtual uint64_t random() PURE; @@ -138,7 +138,7 @@ class HazelcastCache : public HttpCache { * @note Ignored in UNIFIED mode. * @return Size in bytes for a single body entry configured for the cache */ - const uint64_t& bodySizePerEntry() { return body_partition_size_; }; + uint64_t bodySizePerEntry() { return body_partition_size_; }; /** * @return Allowed max size in bytes for a response configured for the cache @@ -146,7 +146,7 @@ class HazelcastCache : public HttpCache { * than this limit, the first max_body_size_ bytes of the response * will be cached only. */ - const uint64_t& maxBodySize() { return max_body_size_; }; + uint64_t maxBodySize() { return max_body_size_; }; /** * Generates a unique signed key for an unsigned one. @@ -154,7 +154,7 @@ class HazelcastCache : public HttpCache { * @return Signed unique key * @note Hazelcast client accepts signed keys only. */ - inline int64_t mapKey(const uint64_t& unsigned_key) { + inline int64_t mapKey(const uint64_t unsigned_key) { // The reason for not static casting directly is a possible overflow // for int64 on intermediate step for -2^63. int64_t signed_key; @@ -174,7 +174,7 @@ class HazelcastCache : public HttpCache { * body for key 1 and the 1st order body for key 11 will have * the same map key "111". */ - inline std::string orderedMapKey(const uint64_t& key, const uint64_t& order) { + inline std::string orderedMapKey(const uint64_t key, const uint64_t order) { return std::to_string(key).append("#").append(std::to_string(order)); } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h index 74ff9b0e3e7ed..64d4f71690b40 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h @@ -51,8 +51,8 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { void readUnifiedData(ObjectDataInput& reader); const Key& variantKey() const { return variant_key_; } - const uint64_t& bodySize() const { return body_size_; } - const int32_t& version() const { return version_; } + uint64_t bodySize() const { return body_size_; } + int32_t version() const { return version_; } Http::ResponseHeaderMapPtr& headerMap() { return header_map_; } void variantKey(Key&& key) { variant_key_ = std::move(key); } @@ -83,7 +83,7 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { * * @note In DIVIDED cache mode, response headers and corresponding bodies will be stored in * different distributed maps. For a response HeaderEntry with 64 bit hash key , bodies - * will be stored with keys "0", "1", "2".. and so on in a contiguous manner. + * will be stored with keys "#0", "#1", "#2".. and so on in a contiguous manner. * Body partition size is fixed and configurable via cache config. On a range request, only * necessary partitions according to the request will be fetched from distributed map, * not the whole response. @@ -106,7 +106,7 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAw size_t length() const { return body_buffer_.size(); } hazelcast::byte* begin() { return body_buffer_.data(); } - const int32_t& version() const { return version_; } + int32_t version() const { return version_; } void bodyBuffer(std::vector&& buffer) { body_buffer_ = std::move(buffer); } void headerKey(int64_t key) { header_key_ = key; } @@ -122,7 +122,7 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAw /** The same hash key with the corresponding header. */ // Not stored in distributed map but used to store related bodies in the same - // node in Hazelcast cluster. Hence (possible) extra networking calls are prevented. + // partition in Hazelcast cluster. int64_t header_key_; /** Derived from header. */ diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index 9d9150614785a..1616d0fa4f7cd 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -1,6 +1,7 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" + #include "common/buffer/buffer_impl.h" -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" #include "extensions/filters/http/cache/hazelcast_http_cache/util.h" namespace Envoy { @@ -11,11 +12,93 @@ namespace HazelcastHttpCache { using Envoy::Protobuf::util::MessageDifferencer; +HazelcastLookupContextBase::HazelcastLookupContextBase(HazelcastCache& cache, + LookupRequest&& request) + : hz_cache_(cache), lookup_request_(std::move(request)) { + createVariantKey(lookup_request_.key()); + variant_hash_key_ = stableHashKey(lookup_request_.key()); +} + +void HazelcastLookupContextBase::getTrailers(LookupTrailersCallback&&) { + // TODO(enozcan): Support trailers when implemented on the filter side. + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; +} + +void HazelcastLookupContextBase::handleLookupFailure(absl::string_view message, + const LookupHeadersCallback& cb, + bool warn_log) { + if (warn_log) { + ENVOY_LOG(warn, "{}", message); + } else { + ENVOY_LOG(debug, "{}", message); + } + abort_insertion_ = true; + cb(LookupResult{}); +} + +void HazelcastLookupContextBase::createVariantKey(Key& raw_key) { + ASSERT(raw_key.custom_fields_size() == 0); + ASSERT(raw_key.custom_ints_size() == 0); // Key must be pure. + if (lookup_request_.vary_headers().size() == 0) { + return; + } + std::vector> header_strings; + + for (const Http::HeaderEntry& header : lookup_request_.vary_headers()) { + header_strings.push_back(std::make_pair(std::string(header.key().getStringView()), + std::string(header.value().getStringView()))); + } + + // Different order of headers causes different hash keys even if their both key and value + // are the same. That is, the following two header lists will cause different hashes for + // the same response and hence they are sorted before insertion. + // + // { {"User-Agent", "desktop"}, {"Accept-Encoding","gzip"} } + // { {"Accept-Encoding","gzip"}, {"User-Agent", "desktop"} } + + std::sort(header_strings.begin(), header_strings.end(), [](auto& left, auto& right) -> bool { + // Per https://tools.ietf.org/html/rfc2616#section-4.2 if two different header entries + // have the same field-name, then their order should not change. For distinct field-named + // headers the order is not significant but sorted alphabetically here to get the same hash + // for the same headers. + return left.first == right.first ? false : left.first < right.first; + }); + + // stableHashKey now creates variant hash for the key since its custom_fields are like: + // [ "Accept-Encoding", "gzip", "User-Agent", "desktop"] + for (auto& header : header_strings) { + raw_key.add_custom_fields(std::move(header.first)); + raw_key.add_custom_fields(std::move(header.second)); + } + + // TODO(enozcan): Ensure the generation of the same key for the same response independent + // from the header orders. + // + // Different hash keys will be created if the order of values differ for the same + // vary header key. The response will not be affected but the same response will + // be cached with different keys. i.e. two different hashes exist for the followings + // where the only allowed vary header is "accept-language": + // - {accept-language: en-US,tr;q=0.8} + // - {accept-language: tr;q=0.8,en-US} +} + +HazelcastInsertContextBase::HazelcastInsertContextBase(LookupContext& lookup_context, + HazelcastCache& cache) + : hz_cache_(cache), max_body_size_(cache.maxBodySize()), + variant_hash_key_(static_cast(lookup_context).variantHashKey()), + variant_key_(static_cast(lookup_context).variantKey()), + abort_insertion_(static_cast(lookup_context).isAborted()) {} + +void HazelcastInsertContextBase::insertTrailers(const Http::ResponseTrailerMap&) { + // TODO(enozcan): Support trailers when implemented on the filter side. + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; +} + UnifiedLookupContext::UnifiedLookupContext(HazelcastCache& cache, LookupRequest&& request) : HazelcastLookupContextBase(cache, std::move(request)) {} void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { - ENVOY_LOG(debug, "Looking up unified response with key {}u", variant_hash_key_); + ENVOY_LOG(debug, "Looking up unified response with key: {}u", variant_hash_key_); try { response_ = hz_cache_.getResponse(variant_hash_key_); } catch (HazelcastClientOfflineException e) { @@ -31,10 +114,8 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { return; } if (response_) { - ENVOY_LOG(debug, - "Found unified response for key {}u, " - "body size = {}", - variant_hash_key_, response_->body().length()); + ENVOY_LOG(debug, "Found unified response: [key: {}u, body size: {}]", variant_hash_key_, + response_->body().length()); if (!MessageDifferencer::Equals(response_->header().variantKey(), variantKey())) { // As cache filter denotes, a secondary check other than the hash key // is performed here. If a different response is found with the same @@ -48,7 +129,7 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { cb(lookup_request_.makeLookupResult(std::move(response_->header().headerMap()), response_->body().length())); } else { - ENVOY_LOG(debug, "Didn't find unified response for key {}u", variant_hash_key_); + ENVOY_LOG(debug, "Missed unified response lookup for key: {}u", variant_hash_key_); // Unlike DIVIDED mode, lock is not tried to be acquired before insertion. // Instead, when putting a unified response into cache, putIfAbsent is called // and hence only one insertion is performed. Cost for the creation of the @@ -59,7 +140,7 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { } void UnifiedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) { - ENVOY_LOG(debug, "Getting unified body (total length = {}) with range from {} to {}", + ENVOY_LOG(debug, "Getting unified body (total length = {}) with range: [{}, {}]", response_->body().length(), range.begin(), range.end()); ASSERT(response_ && !abort_insertion_); ASSERT(range.end() <= response_->body().length()); @@ -78,7 +159,7 @@ void UnifiedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response ASSERT(!committed_end_stream_); header_map_ = Http::createHeaderMap(response_headers); if (end_stream) { - flushEntry(); + insertResponse(); } } @@ -100,22 +181,22 @@ void UnifiedInsertContext::insertBody(const Buffer::Instance& chunk, // Store the body copied until now and abort the further attempted. buffer_vector_.resize(max_body_size_); chunk.copyOut(0, allowed_size, buffer_vector_.data() + buffer_length); - flushEntry(); + insertResponse(); ready_for_next_chunk(false); return; } if (end_stream) { - flushEntry(); + insertResponse(); } else if (ready_for_next_chunk) { ready_for_next_chunk(true); } } -void UnifiedInsertContext::flushEntry() { +void UnifiedInsertContext::insertResponse() { ASSERT(!abort_insertion_); ASSERT(!committed_end_stream_); - ENVOY_LOG(debug, "Inserting unified entry if absent with key {}u", variant_hash_key_); + ENVOY_LOG(debug, "Inserting unified entry with key {}u if absent", variant_hash_key_); committed_end_stream_ = true; // Versions are not necessary for unified entries. Hence passing arbitrary 0 here. @@ -140,7 +221,7 @@ DividedLookupContext::DividedLookupContext(HazelcastCache& cache, LookupRequest& body_partition_size_(cache.bodySizePerEntry()){}; void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { - ENVOY_LOG(debug, "Looking up divided header with key {}u", variant_hash_key_); + ENVOY_LOG(debug, "Looking up divided header with key: {}u", variant_hash_key_); HazelcastHeaderPtr header_entry; try { header_entry = hz_cache_.getHeader(variant_hash_key_); @@ -158,12 +239,14 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { } if (header_entry) { abort_insertion_ = true; // overriding an exiting entry is not allowed. - ENVOY_LOG(debug, "Found divided response for key {}u, version {}, body size = {}", + ENVOY_LOG(debug, "Found divided response: [key: {}u, version: {}, body size: {}]", variant_hash_key_, header_entry->version(), header_entry->bodySize()); if (!MessageDifferencer::Equals(header_entry->variantKey(), variantKey())) { handleLookupFailure("Mismatched keys found for unsigned hash: " + std::to_string(variant_hash_key_), cb, false); + // Unsigned hash is denoted here since entries are stored with signed keys correspond + // to the unsigned ones. This is because of Hazelcast behavior. return; } this->total_body_size_ = header_entry->bodySize(); @@ -171,7 +254,7 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { this->found_header_ = true; cb(lookup_request_.makeLookupResult(std::move(header_entry->headerMap()), total_body_size_)); } else { - ENVOY_LOG(debug, "Didn't find divided response for key {}u", variant_hash_key_); + ENVOY_LOG(debug, "Missed divided response lookup for key: {}u", variant_hash_key_); // To prevent multiple insertion contexts to create the same response in the cache, // mark only one of them responsible for the insertion using Hazelcast map key locks. // If key is not locked, it will be acquired here and only one insertion context @@ -197,14 +280,14 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { // the returning buffer from this function can have a size of at most body_partition_size_. // The caller (filter) has to check range and make another getBody request if needed. // -// For instance, for a response of which body is 5 MB length, the cached entries will look -// like the following with 2 MB of body_partition_size_ configured: +// For instance, for a response of which body is 5 KB length, the cached entries will look +// like the following with 2 KB of body_partition_size_ configured: // // --> HazelcastHeaderEntry(response headers) // -// --> HazelcastBodyEntry(0-2 MB) -// --> HazelcastBodyEntry(2-4 MB) -// --> HazelcastBodyEntry(4-5 MB) +// --> HazelcastBodyEntry(0-2 KB) +// --> HazelcastBodyEntry(2-4 KB) +// --> HazelcastBodyEntry(4-5 KB) // void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) { ASSERT(range.end() <= total_body_size_); @@ -213,6 +296,8 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal // Lookup for only one body partition which includes the range.begin(). uint64_t body_index = range.begin() / body_partition_size_; HazelcastBodyPtr body; + ENVOY_LOG(debug, "Looking up divided body with key: {}u, order: {}", variant_hash_key_, + body_index); try { body = hz_cache_.getBody(variant_hash_key_, body_index); } catch (HazelcastClientOfflineException e) { @@ -229,7 +314,7 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal } if (body) { - ENVOY_LOG(debug, "Found divided body with key {}u + \"{}\", version {}, size {}", + ENVOY_LOG(debug, "Found divided body: [key: {}u + \"#{}\", version: {}, size: {}]", variant_hash_key_, body_index, body->version(), body->length()); if (body->version() != version_) { hz_cache_.onVersionMismatch(variant_hash_key_, version_, total_body_size_); @@ -283,7 +368,7 @@ void DividedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response ASSERT(!committed_end_stream_); header_map_ = Http::createHeaderMap(response_headers); if (end_stream) { - flushHeader(); + insertHeader(); } } @@ -336,7 +421,7 @@ void DividedInsertContext::insertBody(const Buffer::Instance& chunk, // Header insertion is performed only when all bodies are stored. // Otherwise, insertion will be aborted and another insert context // will store the response by overriding body entries flushed so far. - flushHeader(); + insertHeader(); } } if (ready_for_next_chunk) { @@ -344,7 +429,7 @@ void DividedInsertContext::insertBody(const Buffer::Instance& chunk, } } -void DividedInsertContext::copyIntoLocalBuffer(uint64_t& offset, uint64_t& size, +void DividedInsertContext::copyIntoLocalBuffer(uint64_t& offset, uint64_t size, const Buffer::Instance& source) { uint64_t current_size = buffer_vector_.size(); buffer_vector_.resize(current_size + size); @@ -352,12 +437,6 @@ void DividedInsertContext::copyIntoLocalBuffer(uint64_t& offset, uint64_t& size, offset += size; }; -/** - * Wraps the current body buffer with HazelcastBodyEntry and puts - * into the cache. - * - * @return True if insertion is completed. - */ bool DividedInsertContext::flushBuffer() { ASSERT(!abort_insertion_); if (buffer_vector_.size() == 0) { @@ -389,7 +468,7 @@ bool DividedInsertContext::flushBuffer() { return true; } -void DividedInsertContext::flushHeader() { +void DividedInsertContext::insertHeader() { ASSERT(!abort_insertion_); ASSERT(!committed_end_stream_); committed_end_stream_ = true; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h index 9e44927bf39f2..0ecf82a2d0b2f 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h @@ -14,36 +14,19 @@ namespace HazelcastHttpCache { class HazelcastLookupContextBase : public LookupContext, public Logger::Loggable { public: - HazelcastLookupContextBase(HazelcastCache& cache, LookupRequest&& request) - : hz_cache_(cache), lookup_request_(std::move(request)) { - createVariantKey(lookup_request_.key()); - variant_hash_key_ = stableHashKey(lookup_request_.key()); - } + HazelcastLookupContextBase(HazelcastCache& cache, LookupRequest&& request); - void getTrailers(LookupTrailersCallback&&) override { - // TODO(enozcan): Support trailers when implemented on the filter side. - NOT_IMPLEMENTED_GCOVR_EXCL_LINE; - } + // LookupContext + void getTrailers(LookupTrailersCallback&&) override; const LookupRequest& request() const { return lookup_request_; } - const Key& variantKey() const { return lookup_request_.key(); } - uint64_t variantHashKey() const { return variant_hash_key_; } - bool isAborted() const { return abort_insertion_; } protected: void handleLookupFailure(absl::string_view message, const LookupHeadersCallback& cb, - bool warn_log = true) { - if (warn_log) { - ENVOY_LOG(warn, "{}", message); - } else { - ENVOY_LOG(debug, "{}", message); - } - abort_insertion_ = true; - cb(LookupResult{}); - } + bool warn_log = true); HazelcastCache& hz_cache_; LookupRequest lookup_request_; @@ -58,7 +41,7 @@ class HazelcastLookupContextBase : public LookupContext, /** * The keys created by the cache filter for lookups and inserts are not aware * of the vary headers of the request. Instead, cache filter expects a - * cache to differentiate responses having the same key by their vary + * cache plugin to differentiate responses having the same key by their vary * headers. Rather than storing multiple responses with the same key and * then querying them according to vary headers, a different key for each * response including vary headers in custom fields is created here. Hence @@ -66,51 +49,7 @@ class HazelcastLookupContextBase : public LookupContext, * * @param raw_key Key created by the filter. */ - void createVariantKey(Key& raw_key) { - ASSERT(raw_key.custom_fields_size() == 0); - ASSERT(raw_key.custom_ints_size() == 0); // Key must be pure. - if (lookup_request_.vary_headers().size() == 0) { - return; - } - std::vector> header_strings; - - for (const Http::HeaderEntry& header : lookup_request_.vary_headers()) { - header_strings.push_back(std::make_pair(std::string(header.key().getStringView()), - std::string(header.value().getStringView()))); - } - - // Different order of headers causes different hash keys even if their both key and value - // are the same. That is, the following two header lists will cause different hashes for - // the same response and hence they are sorted before insertion. - // - // { {"User-Agent", "desktop"}, {"Accept-Encoding","gzip"} } - // { {"Accept-Encoding","gzip"}, {"User-Agent", "desktop"} } - - std::sort(header_strings.begin(), header_strings.end(), [](auto& left, auto& right) -> bool { - // Per https://tools.ietf.org/html/rfc2616#section-4.2 if two different header entries - // have the same field-name, then their order should not change. For distinct field-named - // headers the order is not significant but sorted alphabetically here to get the same hash - // for the same headers. - return left.first == right.first ? false : left.first < right.first; - }); - - for (auto& header : header_strings) { - raw_key.add_custom_fields(std::move(header.first)); - raw_key.add_custom_fields(std::move(header.second)); - } - // stableHashKey now creates variant hash for the key since its custom_fields are like: - // [ "Accept-Encoding", "gzip", "User-Agent", "desktop"] - - // TODO(enozcan): Ensure the generation of the same key for the same response independent - // from the header orders. - // - // Different hash keys will be created if the order of values differ for the same - // vary header key. The response will not be affected but the same response will - // be cached with different keys. i.e. two different hashes exist for the followings - // where the only allowed vary header is "accept-language": - // - {accept-language: en-US,tr;q=0.8} - // - {accept-language: tr;q=0.8,en-US} - } + void createVariantKey(Key& raw_key); }; /** @@ -119,21 +58,17 @@ class HazelcastLookupContextBase : public LookupContext, class HazelcastInsertContextBase : public InsertContext, public Logger::Loggable { public: - HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastCache& cache) - : hz_cache_(cache), max_body_size_(cache.maxBodySize()), - variant_hash_key_( - static_cast(lookup_context).variantHashKey()), - variant_key_(static_cast(lookup_context).variantKey()), - abort_insertion_(static_cast(lookup_context).isAborted()) {} - - void insertTrailers(const Http::ResponseTrailerMap&) override { - // TODO(enozcan): Support trailers - NOT_IMPLEMENTED_GCOVR_EXCL_LINE; - } + HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastCache& cache); + + // InsertContext + void insertTrailers(const Http::ResponseTrailerMap&) override; protected: HazelcastCache& hz_cache_; + + // From plugin configuration const uint64_t max_body_size_; + bool committed_end_stream_ = false; // From lookup context @@ -145,6 +80,7 @@ class HazelcastInsertContextBase : public InsertContext, /** Body content is first copied into this buffer and then written to distributed map. */ std::vector buffer_vector_; + /** Response headers to be inserted */ Http::ResponseHeaderMapPtr header_map_; }; @@ -158,6 +94,7 @@ class UnifiedLookupContext : public HazelcastLookupContextBase { void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; private: + /** Response to be inserted */ HazelcastResponsePtr response_; }; @@ -172,7 +109,11 @@ class UnifiedInsertContext : public HazelcastInsertContextBase { bool end_stream) override; private: - void flushEntry(); + /** + * Wraps the current response content with HazelcastResponseEntry and puts + * into the cache. + */ + void insertResponse(); }; /** @@ -188,14 +129,13 @@ class DividedLookupContext : public HazelcastLookupContextBase { void handleBodyLookupFailure(absl::string_view message, const LookupBodyCallback& cb, bool warn_log = true); - uint64_t total_body_size_; - + /** Values fetched from the cache after a successful lookup */ + bool found_header_ = false; int32_t version_; + uint64_t total_body_size_; /** Max body size per body entry defined via config. */ const uint64_t body_partition_size_; - - bool found_header_ = false; }; /** @@ -209,14 +149,33 @@ class DividedInsertContext : public HazelcastInsertContextBase { bool end_stream) override; private: - void copyIntoLocalBuffer(uint64_t& index, uint64_t& size, const Buffer::Instance& source); + /** + * Copies bytes from source to local buffer. Insertion to the cache happens after + * the local buffer is full or the end stream is committed by the filter. + * @param offset Byte offset for the source. Updated much after copy. + * @param size Number of bytes to be copied into the local buffer. + * @param source Body content given by the filter. + */ + void copyIntoLocalBuffer(uint64_t& offset, uint64_t size, const Buffer::Instance& source); + + /** + * Wraps the current body buffer with HazelcastBodyEntry and puts + * into the cache. + * + * @return True if insertion is completed. + */ bool flushBuffer(); - void flushHeader(); + + /** + * Wraps the current header map, request key, body size and version values with + * HazelcastHeaderEntry and puts into the cache. + */ + void insertHeader(); /** * Creates a common version for a header and its body entries. * This version denotes the relation between a header and its - * bodies such that they are inserted in the same insert context + * bodies such that they are inserted by the same insert context * for the same lookup in DIVIDED mode. */ int32_t createVersion(); @@ -227,6 +186,7 @@ class DividedInsertContext : public HazelcastInsertContextBase { /** Max body size per body entry defined via config. */ const uint64_t body_partition_size_; + /** Response specific values to be used in the cached entries */ const int32_t version_; uint64_t total_body_size_ = 0; }; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc index 66aa62ebb4be0..6d3f0a15b2a23 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc @@ -1,5 +1,6 @@ -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" #include "extensions/filters/http/cache/hazelcast_http_cache/util.h" namespace Envoy { @@ -30,10 +31,9 @@ void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, NOT_IMPLEMENTED_GCOVR_EXCL_LINE; // TODO(enozcan): Enable when implemented on the filter side. // Depending on the filter's implementation, the cached entry's - // variant_key_ must be updated as well. - // Also, if vary headers change then the hash key of the response - // will change and updating only header map will not be enough in - // this case. + // variant_key_ must be updated as well. Also, if vary headers + // change then the hash key of the response will change and + // updating only header map will not be enough in this case. ASSERT(lookup_context); ASSERT(response_headers); try { @@ -54,7 +54,7 @@ void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, constexpr absl::string_view HazelcastCacheName = "envoy.extensions.http.cache.hazelcast"; // Cluster wide cache statistics should be observed on Hazelcast Management Center. -// Hence they are not stored locally. +// They are not stored locally. CacheInfo HazelcastHttpCache::cacheInfo() const { CacheInfo cache_info; cache_info.name_ = HazelcastCacheName; @@ -62,20 +62,20 @@ CacheInfo HazelcastHttpCache::cacheInfo() const { return cache_info; } -void HazelcastHttpCache::putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) { +void HazelcastHttpCache::putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) { getHeaderMap().set(mapKey(key), entry); } -void HazelcastHttpCache::putBody(const uint64_t& key, const uint64_t& order, +void HazelcastHttpCache::putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) { getBodyMap().set(orderedMapKey(key, order), entry); } -HazelcastHeaderPtr HazelcastHttpCache::getHeader(const uint64_t& key) { +HazelcastHeaderPtr HazelcastHttpCache::getHeader(const uint64_t key) { return getHeaderMap().get(mapKey(key)); } -HazelcastBodyPtr HazelcastHttpCache::getBody(const uint64_t& key, const uint64_t& order) { +HazelcastBodyPtr HazelcastHttpCache::getBody(const uint64_t key, const uint64_t order) { return getBodyMap().get(orderedMapKey(key, order)); } @@ -101,7 +101,7 @@ void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t b getHeaderMap().remove(mapKey(key)); unlock(key); } catch (HazelcastClientOfflineException e) { - // see DividedInsertContext#flushHeader() for left over locks on a connection failure. + // see DividedInsertContext#insertHeader() for left over locks on a connection failure. ENVOY_LOG(warn, "Hazelcast Connection is offline!"); } catch (OperationTimeoutException e) { ENVOY_LOG(warn, "Clean up for missing body has timed out."); @@ -114,25 +114,27 @@ void HazelcastHttpCache::onVersionMismatch(uint64_t key, int32_t version, uint64 onMissingBody(key, version, body_size); } -void HazelcastHttpCache::putResponseIfAbsent(const uint64_t& key, +void HazelcastHttpCache::putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) { getResponseMap().putIfAbsent(mapKey(key), entry); } -HazelcastResponsePtr HazelcastHttpCache::getResponse(const uint64_t& key) { +HazelcastResponsePtr HazelcastHttpCache::getResponse(const uint64_t key) { return getResponseMap().get(mapKey(key)); } -bool HazelcastHttpCache::tryLock(const uint64_t& key) { +bool HazelcastHttpCache::tryLock(const uint64_t key) { // Internal lock mechanism of Hazelcast specific to map and key pair is // used to make exactly one lookup context responsible for insertions and // secure consistency during updateHeaders(). These locks prevent possible // race for multiple cache filters from multiple proxies when they connect // to the same Hazelcast cluster. + // The locks used here are re-entrant. A locked key can be acquired by + // the same thread again and again based on its pid. return unified_ ? getResponseMap().tryLock(mapKey(key)) : getHeaderMap().tryLock(mapKey(key)); } -void HazelcastHttpCache::unlock(const uint64_t& key) { +void HazelcastHttpCache::unlock(const uint64_t key) { // Hazelcast does not allow a thread to unlock a key unless it's the key // owner. To handle this, forceUnlock is called. if (unified_) { @@ -194,32 +196,16 @@ HazelcastHttpCache::~HazelcastHttpCache() { shutdown(true); } void HazelcastHttpCache::updateUnifiedHeaders(LookupContextPtr&& lookup_context, Http::ResponseHeaderMapPtr&& response_headers) { - const uint64_t& key = static_cast(lookup_context.get())->variantHashKey(); - HazelcastResponsePtr response = getResponse(key); - if (!response) { - // Might be evicted in meantime - ENVOY_LOG(debug, "Updating unified headers ({}) is aborted due to lookup miss.", key); - return; - } - HazelcastResponseEntry updated = *response; - updated.header().headerMap(std::move(response_headers)); - // Update headers if no other update is performed in meantime. - getResponseMap().replace(key, updated, *response); + // TODO(enozcan): Implement when ready on the filter side. + ASSERT(!lookup_context); + ASSERT(!response_headers); } void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, Http::ResponseHeaderMapPtr&& response_headers) { - const uint64_t& key = static_cast(lookup_context.get())->variantHashKey(); - HazelcastHeaderPtr stale = getHeader(key); - if (!stale) { - // Might be evicted in meantime - ENVOY_LOG(debug, "Updating divided headers ({}) is aborted due to lookup miss.", key); - return; - } - HazelcastHeaderEntry updated = *stale; - updated.headerMap(std::move(response_headers)); - // Update headers if no other update is performed in meantime. - getHeaderMap().replace(key, updated, *stale); + // TODO(enozcan): Implement when ready on the filter side. + ASSERT(!lookup_context); + ASSERT(!response_headers); } std::string HazelcastHttpCache::constructMapName(const std::string& postfix) { @@ -240,8 +226,8 @@ HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())), cache_config_(config) { body_map_name_ = constructMapName("body"); - header_map_name_ = constructMapName("div-cache"); - response_map_name_ = constructMapName("uni-cache"); + header_map_name_ = constructMapName("div"); + response_map_name_ = constructMapName("uni"); } std::string HazelcastHttpCacheFactory::name() const { return std::string(HazelcastCacheName); } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h index 3724b90921973..3e6ff8432bf61 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h @@ -21,35 +21,34 @@ namespace HazelcastHttpCache { using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; using hazelcast::client::IMap; -// TODO: Consider putting responses into cache with TTL derived from `age` header. -// instead of using a common TTL for all. +// TODO(enozcan): Consider putting responses into cache with TTL derived from `max-age` header +// instead of using a common TTL for all. This is possible during insertion by passing TTL +// amount regardless of the configured TTL on Hazelcast server side. +// i.e: IMap::put(const K &key, const V &value, int64_t ttlInMilliseconds); -// TODO: Mention about max.lease.time option on readme doc. - -class HazelcastHttpCache : /*public HttpCache,*/ public HazelcastCache, +class HazelcastHttpCache : public HazelcastCache, public Logger::Loggable { public: HazelcastHttpCache(HazelcastHttpCacheConfig config); - // Cache::HttpCache + // from Cache::HttpCache LookupContextPtr makeLookupContext(LookupRequest&& request) override; InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; void updateHeaders(LookupContextPtr&& lookup_context, Http::ResponseHeaderMapPtr&& response_headers) override; CacheInfo cacheInfo() const override; - // HazelcastCache - void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) override; - void putBody(const uint64_t& key, const uint64_t& order, - const HazelcastBodyEntry& entry) override; - HazelcastHeaderPtr getHeader(const uint64_t& key) override; - HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order) override; + // from HazelcastCache + void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) override; + void putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) override; + HazelcastHeaderPtr getHeader(const uint64_t key) override; + HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) override; void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) override; void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) override; - void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry) override; - HazelcastResponsePtr getResponse(const uint64_t& key) override; - bool tryLock(const uint64_t& key) override; - void unlock(const uint64_t& key) override; + void putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) override; + HazelcastResponsePtr getResponse(const uint64_t key) override; + bool tryLock(const uint64_t key) override; + void unlock(const uint64_t key) override; uint64_t random() override; void connect(); diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h index 8a218fd583ca7..4450312e2d2f6 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -15,12 +15,22 @@ class ConfigUtil { static uint64_t validPartitionSize(const uint64_t config_value) { return config_value == 0 ? DEFAULT_PARTITION_SIZE - : (config_value > MAX_PARTITION_SIZE) ? MAX_PARTITION_SIZE : config_value; + : (config_value > MAX_ALLOWED_PARTITION_SIZE) ? MAX_ALLOWED_PARTITION_SIZE + : config_value; } static uint64_t validMaxBodySize(const uint64_t config_value, const bool unified) { - uint64_t max_size = unified ? MAX_UNIFIED_BODY_SIZE : MAX_DIVIDED_BODY_SIZE; - return config_value == 0 || (config_value > max_size) ? max_size : config_value; + if (unified) { + // Apply size limitation for single entry (unified response) on the map. + return config_value == 0 || (config_value > MAX_ALLOWED_UNIFIED_BODY_SIZE) + ? MAX_ALLOWED_UNIFIED_BODY_SIZE + : config_value; + } else { + // In divided mode, no upper limit for a body size exists. Instead, the max number of + // partitions for a response is indirectly set by (max_body_size / partition_size) ratio + // in the plugin configuration. + return config_value == 0 ? DEFAULT_MAX_DIVIDED_BODY_SIZE : config_value; + } } static hazelcast::client::ClientConfig @@ -29,6 +39,10 @@ class ConfigUtil { hazelcast::client::ClientConfig config; config.getGroupConfig().setName(cache_config.group_name()); config.getGroupConfig().setPassword(cache_config.group_password()); + for (auto& address : cache_config.addresses()) { + config.getNetworkConfig().addAddress( + hazelcast::client::Address(address.ip(), address.port())); + } config.getNetworkConfig().setConnectionTimeout(cache_config.connection_timeout() == 0 ? DEFAULT_CONNECTION_TIMEOUT_MS : cache_config.connection_timeout()); @@ -40,10 +54,6 @@ class ConfigUtil { : cache_config.connection_attempt_period()); config.getConnectionStrategyConfig().setReconnectMode( hazelcast::client::config::ClientConnectionStrategyConfig::ReconnectMode::ASYNC); - for (auto& address : cache_config.addresses()) { - config.getNetworkConfig().addAddress( - hazelcast::client::Address(address.ip(), address.port())); - } config.setProperty("hazelcast.client.invocation.timeout.seconds", std::to_string(cache_config.invocation_timeout() == 0 ? DEFAULT_INVOCATION_TIMEOUT_SEC @@ -58,16 +68,20 @@ class ConfigUtil { // a suggestion log will be appeared to increase partition size. static constexpr uint16_t PARTITION_WARN_LIMIT = 16; - // Sizes for each divided body entry. - static constexpr uint64_t DEFAULT_PARTITION_SIZE = 2048; + // Default size for each divided body entry. + static constexpr uint64_t DEFAULT_PARTITION_SIZE = 1024 * 16; - static constexpr uint64_t MAX_PARTITION_SIZE = DEFAULT_PARTITION_SIZE * 32; + // IMap is not optimized for large value sizes. Hence an upper limit is introduced for + // each stored body entry. + // Maximum allowed size for each divided body entry. + static constexpr uint64_t MAX_ALLOWED_PARTITION_SIZE = 1024 * 32; - // Size for total body size of a unified response. - static constexpr uint64_t MAX_UNIFIED_BODY_SIZE = MAX_PARTITION_SIZE; + // The limit to keep a single map entry size reasonable. + // Maximum allowed total body size of a unified response. + static constexpr uint64_t MAX_ALLOWED_UNIFIED_BODY_SIZE = 1024 * 32; - // Size for total body size of a divided response (at most 32 partitions allowed). - static constexpr uint64_t MAX_DIVIDED_BODY_SIZE = MAX_UNIFIED_BODY_SIZE * 32; + // Default maximum body size of a divided response. + static constexpr uint64_t DEFAULT_MAX_DIVIDED_BODY_SIZE = 1024 * 256; // Duration to try to reconnect a cluster if a member does not respond. static constexpr uint32_t DEFAULT_CONNECTION_TIMEOUT_MS = 5000; diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 10097f298a832..19ff62be93b7f 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -15,8 +15,6 @@ envoy_extension_cc_test( extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", - "//test/test_common:simulated_time_system_lib", - "//test/test_common:utility_lib", ], ) @@ -26,8 +24,6 @@ envoy_extension_cc_test( extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", - "//test/test_common:simulated_time_system_lib", - "//test/test_common:utility_lib", ], ) @@ -37,8 +33,6 @@ envoy_extension_cc_test( extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", - "//test/test_common:simulated_time_system_lib", - "//test/test_common:utility_lib", ], ) @@ -61,5 +55,7 @@ envoy_extension_cc_test_library( extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ "//source/extensions/filters/http/cache/hazelcast_http_cache:hazelcast_http_cache_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", ], ) diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc index badc3767f4982..d448ff1831121 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc @@ -13,9 +13,9 @@ using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; class ConfigUtilsTest : public testing::Test { protected: uint64_t defaultPartitionSize() { return ConfigUtil::DEFAULT_PARTITION_SIZE; } - uint64_t maxPartitionSize() { return ConfigUtil::MAX_PARTITION_SIZE; } - uint64_t maxUnifiedBodySize() { return ConfigUtil::MAX_UNIFIED_BODY_SIZE; } - uint64_t maxDividedBodySize() { return ConfigUtil::MAX_DIVIDED_BODY_SIZE; } + uint64_t maxPartitionSize() { return ConfigUtil::MAX_ALLOWED_PARTITION_SIZE; } + uint64_t maxUnifiedBodySize() { return ConfigUtil::MAX_ALLOWED_UNIFIED_BODY_SIZE; } + uint64_t defaultMaxDividedBodySize() { return ConfigUtil::DEFAULT_MAX_DIVIDED_BODY_SIZE; } uint32_t defaultConnectionTimeoutMs() { return ConfigUtil::DEFAULT_CONNECTION_TIMEOUT_MS; } uint32_t defaultConnectionAttemptLimit() { return ConfigUtil::DEFAULT_CONNECTION_ATTEMPT_LIMIT; } uint32_t defaultConnectionAttemptPeriodMs() { @@ -35,17 +35,27 @@ TEST_F(ConfigUtilsTest, ValidPartitionSizeTest) { } TEST_F(ConfigUtilsTest, ValidMaxBodySizeTest) { - auto validateBodySizeTest = [this](bool unified) { - uint64_t max_size = unified ? maxUnifiedBodySize() : maxDividedBodySize(); - uint64_t valid_value = ConfigUtil::validMaxBodySize(0, unified); + { + // unified + uint64_t max_size = maxUnifiedBodySize(); + uint64_t valid_value = ConfigUtil::validMaxBodySize(0, true); EXPECT_EQ(max_size, valid_value); - valid_value = ConfigUtil::validMaxBodySize(max_size + 1, unified); + valid_value = ConfigUtil::validMaxBodySize(max_size + 1, true); EXPECT_EQ(max_size, valid_value); - valid_value = ConfigUtil::validMaxBodySize(max_size - 1, unified); + valid_value = ConfigUtil::validMaxBodySize(max_size - 1, true); EXPECT_EQ(max_size - 1, valid_value); - }; - validateBodySizeTest(true); - validateBodySizeTest(false); + } + { + // divided + uint64_t default_max_size = defaultMaxDividedBodySize(); + uint64_t valid_value = ConfigUtil::validMaxBodySize(0, false); + EXPECT_EQ(default_max_size, valid_value); + valid_value = ConfigUtil::validMaxBodySize(default_max_size + 1, false); + // there is no upper limit for the configured max body size in divided mode. + EXPECT_EQ(default_max_size + 1, valid_value); + valid_value = ConfigUtil::validMaxBodySize(default_max_size - 1, false); + EXPECT_EQ(default_max_size - 1, valid_value); + } } TEST_F(ConfigUtilsTest, ClientConfigTest) { @@ -63,6 +73,8 @@ TEST_F(ConfigUtilsTest, ClientConfigTest) { hazelcast::client::ClientConfig config = ConfigUtil::getClientConfig(default_cache_config); + // Defaults below are not defined by Hazelcast but the cache plugin. So, the below statements + // test if plugin sets the Hazelcast client configuration properly. EXPECT_EQ(defaultConnectionTimeoutMs(), config.getNetworkConfig().getConnectionTimeout()); EXPECT_EQ(defaultConnectionAttemptLimit(), config.getNetworkConfig().getConnectionAttemptLimit()); EXPECT_EQ(defaultConnectionAttemptPeriodMs(), diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 47e36e89a558d..9d46b6f157407 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -30,9 +30,10 @@ class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, INSTANTIATE_TEST_SUITE_P(CommonCacheTests, HazelcastHttpCacheTest, ::testing::Bool()); TEST_P(HazelcastHttpCacheTest, MissPutAndGetEntries) { - const std::string RequestPath1("/size/plus/one"); - const std::string RequestPath2("/exactly/size"); - const std::string RequestPath3("/size/minus/one"); + // To test divided body behavior as well, bodies having sizes near the limit are preferred. + const std::string RequestPath1("/body/with/limit/size/plus/one"); + const std::string RequestPath2("/body/with/exactly/limit/size"); + const std::string RequestPath3("/body/with/limit/size/minus/one"); LookupContextPtr lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -50,9 +51,9 @@ TEST_P(HazelcastHttpCacheTest, MissPutAndGetEntries) { insert(move(lookup_context2), getResponseHeaders(), Body2); insert(move(lookup_context3), getResponseHeaders(), Body3); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath1).get(), Body1)); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath3).get(), Body3)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath1).get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath2).get(), Body2)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath3).get(), Body3)); } TEST_P(HazelcastHttpCacheTest, HandleRangedResponses) { @@ -99,7 +100,7 @@ TEST_P(HazelcastHttpCacheTest, SimplePutGet) { const std::string Body1("hazelcast"); insert(move(name_lookup_context), getResponseHeaders(), Body1); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath1).get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath1).get(), Body1)); const std::string RequestPath2("/simple/put/second"); name_lookup_context = lookup(RequestPath2); @@ -107,7 +108,7 @@ TEST_P(HazelcastHttpCacheTest, SimplePutGet) { const std::string Body2("hazelcast.http.cache"); insert(move(name_lookup_context), getResponseHeaders(), Body2); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath2).get(), Body2)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath2).get(), Body2)); } TEST_P(HazelcastHttpCacheTest, PrivateResponse) { @@ -119,7 +120,7 @@ TEST_P(HazelcastHttpCacheTest, PrivateResponse) { const std::string Body("Value"); insert(move(name_lookup_context), getResponseHeaders(), Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(request_path).get(), Body)); } TEST_P(HazelcastHttpCacheTest, Miss) { @@ -157,7 +158,7 @@ TEST_P(HazelcastHttpCacheTest, RequestSmallMinFresh) { {"cache-control", "public, max-age=9000"}}; const std::string Body("content"); insert(move(name_lookup_context), response_headers, Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(request_path).get(), Body)); } TEST_P(HazelcastHttpCacheTest, ResponseStaleWithRequestLargeMaxStale) { @@ -173,7 +174,7 @@ TEST_P(HazelcastHttpCacheTest, ResponseStaleWithRequestLargeMaxStale) { const std::string Body("content"); insert(move(name_lookup_context), response_headers, Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(request_path).get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(request_path).get(), Body)); } TEST_P(HazelcastHttpCacheTest, StreamingPutAndRangeGet) { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 424d22dc3d7a1..98bb661d1818d 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -35,14 +35,10 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { [&](bool ready) { ready_for_next = ready; }, false); } - EXPECT_EQ( - (HazelcastTestUtil::TEST_MAX_BODY_SIZE / HazelcastTestUtil::TEST_PARTITION_SIZE) + - ((HazelcastTestUtil::TEST_MAX_BODY_SIZE % HazelcastTestUtil::TEST_PARTITION_SIZE) == 0 - ? 0 - : 1), - cache_->bodyTestMapSize()); - - EXPECT_TRUE(expectLookupSuccessWithBody( + EXPECT_EQ(((HazelcastTestUtil::TEST_MAX_BODY_SIZE + HazelcastTestUtil::TEST_PARTITION_SIZE - 1) / + HazelcastTestUtil::TEST_PARTITION_SIZE), + cache_->bodyTestMapSize()); + EXPECT_TRUE(expectLookupSuccessWithFullBody( lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_MAX_BODY_SIZE, 'h'))); } @@ -56,10 +52,11 @@ TEST_F(HazelcastDividedCacheTest, PreventOverridingCacheEntries) { lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - // A possible call to insertion below is filter's fault, not an expected behavior. + // A possible call to insertion below (using a success lookup context) would be the filter's + // fault, not an expected behavior. const std::string OverriddenBody(HazelcastTestUtil::TEST_PARTITION_SIZE * 3, 'z'); insert(move(lookup_context), getResponseHeaders(), OverriddenBody); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), OriginalBody)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath).get(), OriginalBody)); EXPECT_EQ(2, cache_->bodyTestMapSize()); EXPECT_EQ(1, cache_->headerTestMapSize()); } @@ -79,7 +76,6 @@ TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { // the lock even if it's already locked. This is because the key locks on Hazelcast // IMap are re-entrant. A locked key can be acquired by the same thread again and // again based on its pid. - // TODO: Examine Envoy's threading model to ensure this case's safety. lookup_context2 = lookup(RequestPath); }); t1.join(); @@ -94,7 +90,7 @@ TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { // first one must do the insertion. insert(move(lookup_context1), getResponseHeaders(), Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath).get(), Body)); } TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { @@ -108,7 +104,7 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { const std::string Body(HazelcastTestUtil::TEST_PARTITION_SIZE * 2, 'h'); insert(move(lookup_context), getResponseHeaders(), Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath1).get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath1).get(), Body)); // Change version of the second partition. auto body2 = cache_->base().getBody(variant_hash_key, 1); @@ -141,7 +137,7 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { const std::string Body("hazelcast"); insert(move(lookup_context), getResponseHeaders(), Body); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath).get(), Body)); // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. @@ -162,6 +158,9 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); EXPECT_EQ(1, cache_->headerTestMapSize()); + + auto modified_header = cache_->base().getHeader(variant_hash_key); + EXPECT_EQ(2, modified_header->variantKey().custom_fields_size()); } TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { @@ -183,7 +182,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { // variant_hash_key "0" -> Body1 (in body map) // variant_hash_key "1" -> Body2 (in body map) // variant_hash_key "2" -> Body3 (in body map) - EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); cache_->removeTestBody(variant_hash_key, 1); // evict Body2. @@ -206,7 +205,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { // explicitly or let context do the insertion and then release. // If not released, the second run for the test fails. Since no // insertion follows the missed lookup here, the lock is explicitly - // unlocked. + // released. cache_->base().unlock(variant_hash_key); // Assert clean up @@ -232,18 +231,19 @@ TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { headerOnlyTest("/empty/body/response", true); EXPECT_EQ(0, cache_->bodyTestMapSize()); + EXPECT_EQ(2, cache_->headerTestMapSize()); } TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { { - const std::string RequestPath("/online/offline/then/online"); + const std::string RequestPath("/connection/lost/after/insertion"); LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); const std::string Body("s", HazelcastTestUtil::TEST_PARTITION_SIZE); insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); cache_->dropTestConnection(); @@ -254,25 +254,26 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { cache_->restoreTestConnection(); lookup_context = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); } { - const std::string RequestPath("/connection/lost/during/body/insert"); + const std::string RequestPath("/connection/lost/during/insertion"); InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup(RequestPath)); insert_context->insertHeaders(getResponseHeaders(), false); - insert_context->insertBody( - Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h')), [](bool) {}, - false); - insert_context->insertBody( - Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z')), [](bool) {}, - false); + auto insert = [&insert_context](std::string body, bool end_stream) { + insert_context->insertBody( + Buffer::OwnedImpl(body), [](bool) {}, end_stream); + }; + + insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'), false); + insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z'), false); cache_->dropTestConnection(); - insert_context->insertBody( - Buffer::OwnedImpl(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c')), [](bool) {}, - false); + insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'), false); + insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 's'), true); + LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc index d5796a0971bee..13396623b2130 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc @@ -1,7 +1,7 @@ -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" - #include "test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -74,19 +74,18 @@ void HazelcastTestableLocalCache::updateHeaders(LookupContextPtr&& lookup_contex CacheInfo HazelcastTestableLocalCache::cacheInfo() const { return CacheInfo(); } -void HazelcastTestableLocalCache::putHeader(const uint64_t& key, - const HazelcastHeaderEntry& entry) { +void HazelcastTestableLocalCache::putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) { checkConnection(); headerMap[mapKey(key)] = HazelcastHeaderPtr(new HazelcastHeaderEntry(entry)); } -void HazelcastTestableLocalCache::putBody(const uint64_t& key, const uint64_t& order, +void HazelcastTestableLocalCache::putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) { checkConnection(); bodyMap[orderedMapKey(key, order)] = HazelcastBodyPtr(new HazelcastBodyEntry(entry)); } -HazelcastHeaderPtr HazelcastTestableLocalCache::getHeader(const uint64_t& key) { +HazelcastHeaderPtr HazelcastTestableLocalCache::getHeader(const uint64_t key) { checkConnection(); auto result = headerMap.find(mapKey(key)); if (result != headerMap.end()) { @@ -97,7 +96,7 @@ HazelcastHeaderPtr HazelcastTestableLocalCache::getHeader(const uint64_t& key) { } } -HazelcastBodyPtr HazelcastTestableLocalCache::getBody(const uint64_t& key, const uint64_t& order) { +HazelcastBodyPtr HazelcastTestableLocalCache::getBody(const uint64_t key, const uint64_t order) { checkConnection(); auto result = bodyMap.find(orderedMapKey(key, order)); if (result != bodyMap.end()) { @@ -128,7 +127,7 @@ void HazelcastTestableLocalCache::onVersionMismatch(uint64_t key, int32_t versio onMissingBody(key, version, body_size); } -void HazelcastTestableLocalCache::putResponseIfAbsent(const uint64_t& key, +void HazelcastTestableLocalCache::putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) { checkConnection(); if (responseMap.find(mapKey(key)) != responseMap.end()) { @@ -137,7 +136,7 @@ void HazelcastTestableLocalCache::putResponseIfAbsent(const uint64_t& key, responseMap[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); } -HazelcastResponsePtr HazelcastTestableLocalCache::getResponse(const uint64_t& key) { +HazelcastResponsePtr HazelcastTestableLocalCache::getResponse(const uint64_t key) { checkConnection(); auto result = responseMap.find(mapKey(key)); if (result != responseMap.end()) { @@ -147,7 +146,7 @@ HazelcastResponsePtr HazelcastTestableLocalCache::getResponse(const uint64_t& ke } } -bool HazelcastTestableLocalCache::tryLock(const uint64_t& key) { +bool HazelcastTestableLocalCache::tryLock(const uint64_t key) { checkConnection(); if (unified_) { bool locked = std::find(responseLocks.begin(), responseLocks.end(), key) != responseLocks.end(); @@ -168,7 +167,7 @@ bool HazelcastTestableLocalCache::tryLock(const uint64_t& key) { } } -void HazelcastTestableLocalCache::unlock(const uint64_t& key) { +void HazelcastTestableLocalCache::unlock(const uint64_t key) { checkConnection(); if (unified_) { responseLocks.erase(std::remove(responseLocks.begin(), responseLocks.end(), key), @@ -178,7 +177,7 @@ void HazelcastTestableLocalCache::unlock(const uint64_t& key) { } } -uint64_t HazelcastTestableLocalCache::random() { return std::rand(); } +uint64_t HazelcastTestableLocalCache::random() { return ++random_counter_; } } // namespace HazelcastHttpCache } // namespace Cache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h index 9827b625d2765..a5162f644db61 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h @@ -63,17 +63,16 @@ class HazelcastTestableLocalCache : public HazelcastTestableHttpCache, public Ha CacheInfo cacheInfo() const override; // HazelcastCache - void putHeader(const uint64_t& key, const HazelcastHeaderEntry& entry) override; - void putBody(const uint64_t& key, const uint64_t& order, - const HazelcastBodyEntry& entry) override; - HazelcastHeaderPtr getHeader(const uint64_t& key) override; - HazelcastBodyPtr getBody(const uint64_t& key, const uint64_t& order) override; + void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) override; + void putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) override; + HazelcastHeaderPtr getHeader(const uint64_t key) override; + HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) override; void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) override; void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) override; - void putResponseIfAbsent(const uint64_t& key, const HazelcastResponseEntry& entry) override; - HazelcastResponsePtr getResponse(const uint64_t& key) override; - bool tryLock(const uint64_t& key) override; - void unlock(const uint64_t& key) override; + void putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) override; + HazelcastResponsePtr getResponse(const uint64_t key) override; + bool tryLock(const uint64_t key) override; + void unlock(const uint64_t key) override; uint64_t random() override; private: @@ -91,6 +90,7 @@ class HazelcastTestableLocalCache : public HazelcastTestableHttpCache, public Ha std::vector responseLocks; bool connected_ = false; + uint32_t random_counter_ = 0; }; /** diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 89706497abee6..8f1374205c5e6 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -36,7 +36,7 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedInsertionWhenMaxSizeReached) { [&](bool ready) { ready_for_next = ready; }, false); } - EXPECT_TRUE(expectLookupSuccessWithBody( + EXPECT_TRUE(expectLookupSuccessWithFullBody( lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_MAX_BODY_SIZE, 'h'))); } @@ -54,12 +54,12 @@ TEST_F(HazelcastUnifiedCacheTest, PutResponseOnlyWhenAbsent) { // The second context should insert if the cache is empty for this request. insert(move(lookup_context1), getResponseHeaders(), Body1); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath).get(), Body1)); // The first context should not do the insertion/override the existing value. insert(move(lookup_context2), getResponseHeaders(), Body2); // Response body must remain as Body1 - EXPECT_TRUE(expectLookupSuccessWithBody(lookup(RequestPath).get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath).get(), Body1)); } TEST_F(HazelcastUnifiedCacheTest, DoNotOverrideExistingResponse) { @@ -77,7 +77,7 @@ TEST_F(HazelcastUnifiedCacheTest, DoNotOverrideExistingResponse) { insert(move(lookup_context2), getResponseHeaders(), Body2); lookup_context1 = lookup(RequestPath1); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body1)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body1)); } TEST_F(HazelcastUnifiedCacheTest, UnifiedHeaderOnlyResponse) { @@ -101,7 +101,7 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { const std::string Body("hazelcast"); insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context.get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. @@ -132,7 +132,7 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { const std::string Body("s", HazelcastTestUtil::TEST_PARTITION_SIZE); insert(move(lookup_context1), getResponseHeaders(), Body); lookup_context1 = lookup(RequestPath1); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); cache_->dropTestConnection(); @@ -143,7 +143,7 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { cache_->restoreTestConnection(); lookup_context1 = lookup(RequestPath1); - EXPECT_TRUE(expectLookupSuccessWithBody(lookup_context1.get(), Body)); + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); } } // namespace HazelcastHttpCache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h index e0ebb4eb53c8d..74fda06905b64 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -123,8 +123,13 @@ class HazelcastHttpCacheTestBase : public testing::Test { return LookupRequest(request_headers_, current_time_); } - AssertionResult expectLookupSuccessWithBody(LookupContext* lookup_context, - absl::string_view body) { + AssertionResult expectLookupSuccessWithFullBody(LookupContext* lookup_context, + absl::string_view body) { + if (lookup_result_.content_length_ != body.size()) { + return AssertionFailure() << "Expected: lookup_result_.content_length_" + " == " + << body.size() << "\n Actual: " << lookup_result_.content_length_; + } // From SimpleHttpCacheTest if (lookup_result_.cache_entry_status_ != CacheEntryStatus::Ok) { return AssertionFailure() << "Expected: lookup_result_.cache_entry_status" From a3a1af44c64cfcb86818e0c0137c8899b61a9e3c Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Wed, 15 Apr 2020 21:49:26 +0300 Subject: [PATCH 08/33] Fix clang_tidy errors. Signed-off-by: Enes Ozcan --- .../hazelcast_cache_entry.cc | 4 +- .../hazelcast_cache_entry.h | 34 +++++------ .../hazelcast_http_cache/hazelcast_context.cc | 33 ++++++----- .../hazelcast_http_cache_impl.cc | 10 ++-- .../hazelcast_http_cache_impl.h | 2 +- .../hazelcast_common_cache_test.cc | 2 +- .../hazelcast_divided_cache_test.cc | 2 +- .../hazelcast_test_cache.cc | 58 ++++++++++--------- .../hazelcast_test_cache.h | 10 ++-- .../hazelcast_unified_cache_test.cc | 2 +- .../http/cache/hazelcast_http_cache/util.h | 3 +- 11 files changed, 82 insertions(+), 78 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc index 5cb1ee0c4fb87..db0588fb5971f 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc @@ -74,7 +74,7 @@ HazelcastHeaderEntry::HazelcastHeaderEntry(const HazelcastHeaderEntry& other) { version_ = other.version_; } -HazelcastHeaderEntry::HazelcastHeaderEntry(HazelcastHeaderEntry&& other) +HazelcastHeaderEntry::HazelcastHeaderEntry(HazelcastHeaderEntry&& other) noexcept : header_map_(std::move(other.header_map_)), variant_key_(std::move(other.variant_key_)), body_size_(other.body_size_), version_(other.version_) {} @@ -108,7 +108,7 @@ HazelcastBodyEntry::HazelcastBodyEntry(const HazelcastBodyEntry& other) { version_ = other.version_; } -HazelcastBodyEntry::HazelcastBodyEntry(HazelcastBodyEntry&& other) +HazelcastBodyEntry::HazelcastBodyEntry(HazelcastBodyEntry&& other) noexcept : header_key_(other.header_key_), version_(other.version_), body_buffer_(std::move(other.body_buffer_)) {} diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h index 64d4f71690b40..eb09c5a67a3e4 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h @@ -39,11 +39,11 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { HazelcastHeaderEntry(Http::ResponseHeaderMapPtr&& header_map, Key&& key, uint64_t body_size, int32_t version); HazelcastHeaderEntry(const HazelcastHeaderEntry& other); - HazelcastHeaderEntry(HazelcastHeaderEntry&& other); + HazelcastHeaderEntry(HazelcastHeaderEntry&& other) noexcept; // hazelcast::client::serialization::IdentifiedDataSerializable - void writeData(ObjectDataOutput& writer) const; - void readData(ObjectDataInput& reader); + void writeData(ObjectDataOutput& writer) const override; + void readData(ObjectDataInput& reader) override; // Only required fields of a header entry for unified mode are // de/serialized in unifiedData methods. @@ -61,8 +61,8 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { private: // hazelcast::client::serialization::IdentifiedDataSerializable - int getClassId() const { return HAZELCAST_HEADER_TYPE_ID; } - int getFactoryId() const { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + int getClassId() const override { return HAZELCAST_HEADER_TYPE_ID; } + int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } Http::ResponseHeaderMapPtr header_map_; @@ -93,11 +93,11 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAw HazelcastBodyEntry(); HazelcastBodyEntry(int64_t header_key, std::vector&& buffer, int32_t version); HazelcastBodyEntry(const HazelcastBodyEntry& other); - HazelcastBodyEntry(HazelcastBodyEntry&& other); + HazelcastBodyEntry(HazelcastBodyEntry&& other) noexcept; // hazelcast::client::serialization::IdentifiedDataSerializable - void writeData(ObjectDataOutput& writer) const; - void readData(ObjectDataInput& reader); + void writeData(ObjectDataOutput& writer) const override; + void readData(ObjectDataInput& reader) override; // Only required fields of a body entry for unified mode are // de/serialized in unifiedData methods. @@ -114,11 +114,11 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAw private: // hazelcast::client::serialization::IdentifiedDataSerializable - int getClassId() const { return HAZELCAST_BODY_TYPE_ID; } - int getFactoryId() const { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + int getClassId() const override { return HAZELCAST_BODY_TYPE_ID; } + int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } // hazelcast::client::PartitionAware - const int64_t* getPartitionKey() const { return &header_key_; } + const int64_t* getPartitionKey() const override { return &header_key_; } /** The same hash key with the corresponding header. */ // Not stored in distributed map but used to store related bodies in the same @@ -149,10 +149,10 @@ class HazelcastResponseEntry : public IdentifiedDataSerializable { private: // hazelcast::client::serialization::IdentifiedDataSerializable - void writeData(ObjectDataOutput& writer) const; - void readData(ObjectDataInput& reader); - int getClassId() const { return HAZELCAST_RESPONSE_TYPE_ID; } - int getFactoryId() const { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + void writeData(ObjectDataOutput& writer) const override; + void readData(ObjectDataInput& reader) override; + int getClassId() const override { return HAZELCAST_RESPONSE_TYPE_ID; } + int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } HazelcastHeaderEntry response_header_; HazelcastBodyEntry response_body_; @@ -172,8 +172,8 @@ class HazelcastCacheEntrySerializableFactory : public DataSerializableFactory { public: static const int FACTORY_ID = HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; - virtual std::auto_ptr create(int32_t classId) { - switch (classId) { + std::auto_ptr create(int32_t class_id) override { + switch (class_id) { case HAZELCAST_BODY_TYPE_ID: return std::auto_ptr(new HazelcastBodyEntry()); case HAZELCAST_HEADER_TYPE_ID: diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index 1616d0fa4f7cd..70273aec19eea 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -39,7 +39,7 @@ void HazelcastLookupContextBase::handleLookupFailure(absl::string_view message, void HazelcastLookupContextBase::createVariantKey(Key& raw_key) { ASSERT(raw_key.custom_fields_size() == 0); ASSERT(raw_key.custom_ints_size() == 0); // Key must be pure. - if (lookup_request_.vary_headers().size() == 0) { + if (lookup_request_.vary_headers().empty()) { return; } std::vector> header_strings; @@ -109,8 +109,8 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { } catch (OperationTimeoutException e) { handleLookupFailure("Operation timed out during cache lookup.", cb); return; - } catch (...) { - handleLookupFailure("Lookup to cache has failed.", cb); + } catch (std::exception& e) { + handleLookupFailure(fmt::format("Lookup to cache has failed: {}", e.what()), cb); return; } if (response_) { @@ -211,8 +211,8 @@ void UnifiedInsertContext::insertResponse() { ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Failed to insert response."); } catch (OperationTimeoutException e) { ENVOY_LOG(warn, "Operation timed out during cache insertion."); - } catch (...) { - ENVOY_LOG(warn, "Response insertion to cache has failed."); + } catch (std::exception& e) { + ENVOY_LOG(warn, "Response insertion to cache has failed: {}", e.what()); } } @@ -233,8 +233,8 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { } catch (OperationTimeoutException e) { handleLookupFailure("Operation timed out during cache lookup.", cb); return; - } catch (...) { - handleLookupFailure("Lookup to cache has failed.", cb); + } catch (std::exception& e) { + handleLookupFailure(fmt::format("Lookup to cache has failed: {}", e.what()), cb); return; } if (header_entry) { @@ -268,8 +268,8 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { " until the connection is restored...", cb); return; - } catch (...) { - handleLookupFailure("Lock trial has failed.", cb); + } catch (std::exception& e) { + handleLookupFailure(fmt::format("Lock trial has failed: {}", e.what()), cb); return; } cb(LookupResult{}); @@ -308,8 +308,9 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal } catch (OperationTimeoutException e) { handleBodyLookupFailure("Operation timed out during cache lookup.", cb); return; - } catch (...) { - handleBodyLookupFailure("Lookup to cache for body entry has failed.", cb); + } catch (std::exception& e) { + handleBodyLookupFailure(fmt::format("Lookup to cache for body entry has failed: {}", e.what()), + cb); return; } @@ -439,7 +440,7 @@ void DividedInsertContext::copyIntoLocalBuffer(uint64_t& offset, uint64_t size, bool DividedInsertContext::flushBuffer() { ASSERT(!abort_insertion_); - if (buffer_vector_.size() == 0) { + if (buffer_vector_.empty()) { return true; } total_body_size_ += buffer_vector_.size(); @@ -454,8 +455,8 @@ bool DividedInsertContext::flushBuffer() { } catch (OperationTimeoutException e) { ENVOY_LOG(warn, "Operation timed out during body insertion."); return false; - } catch (...) { - ENVOY_LOG(warn, "Body insertion to cache has failed."); + } catch (std::exception& e) { + ENVOY_LOG(warn, "Body insertion to cache has failed: {}", e.what()); return false; } if (body_order_ == ConfigUtil::partitionWarnLimit()) { @@ -486,8 +487,8 @@ void DividedInsertContext::insertHeader() { // option can be used when available in a future release of cpp client. The related // issue can be tracked at: https://github.com/hazelcast/hazelcast-cpp-client/issues/579 // TODO(enozcan): Use tryLock with leaseTime when released for Hazelcast cpp client. - } catch (...) { - ENVOY_LOG(warn, "Failed to complete response insertion."); + } catch (std::exception& e) { + ENVOY_LOG(warn, "Failed to complete response insertion: {}", e.what()); } } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc index 6d3f0a15b2a23..e89004efccb2c 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc @@ -46,8 +46,8 @@ void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, ENVOY_LOG(warn, "Hazelcast Connection is offline!"); } catch (OperationTimeoutException e) { ENVOY_LOG(warn, "Updating headers has timed out."); - } catch (...) { - ENVOY_LOG(warn, "Updating headers has failed."); + } catch (std::exception& e) { + ENVOY_LOG(warn, "Updating headers has failed: {}", e.what()); } } @@ -105,8 +105,8 @@ void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t b ENVOY_LOG(warn, "Hazelcast Connection is offline!"); } catch (OperationTimeoutException e) { ENVOY_LOG(warn, "Clean up for missing body has timed out."); - } catch (...) { - ENVOY_LOG(warn, "Clean up for missing body has failed."); + } catch (std::exception& e) { + ENVOY_LOG(warn, "Clean up for missing body has failed: {}", e.what()); } } @@ -242,8 +242,8 @@ HttpCache& HazelcastHttpCacheFactory::getCache( HazelcastHttpCacheConfig hz_cache_config; MessageUtil::unpackTo(config.typed_config(), hz_cache_config); cache_ = std::make_unique(hz_cache_config); + cache_->connect(); } - cache_->connect(); return *cache_; } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h index 3e6ff8432bf61..e2af7100853d8 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h @@ -54,7 +54,7 @@ class HazelcastHttpCache : public HazelcastCache, void connect(); void shutdown(bool destroy); - ~HazelcastHttpCache(); + ~HazelcastHttpCache() override; private: friend class HazelcastHttpCacheTestBase; diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 9d46b6f157407..412e7f4766ebb 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -16,7 +16,7 @@ namespace HazelcastHttpCache { class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, public testing::WithParamInterface { protected: - void SetUp() { + void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(GetParam()); // To test the cache with a real Hazelcast instance, remote cache // must be used during tests. diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 98bb661d1818d..96048c2c1ea5c 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -13,7 +13,7 @@ namespace HazelcastHttpCache { */ class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { protected: - void SetUp() { + void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(false); // To test the cache with a real Hazelcast instance, remote cache // must be used during tests. diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc index 13396623b2130..da6473067ef6c 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc @@ -13,9 +13,9 @@ HazelcastTestableLocalCache::HazelcastTestableLocalCache(HazelcastHttpCacheConfi HazelcastCache(config.unified(), config.body_partition_size(), config.max_body_size()) {} void HazelcastTestableLocalCache::clearTestMaps() { - headerMap.clear(); - bodyMap.clear(); - responseMap.clear(); + header_map_.clear(); + body_map_.clear(); + response_map_.clear(); } void HazelcastTestableLocalCache::dropTestConnection() { connected_ = false; } @@ -24,28 +24,28 @@ void HazelcastTestableLocalCache::restoreTestConnection() { connected_ = true; } int HazelcastTestableLocalCache::headerTestMapSize() { ASSERT(!unified_); - return headerMap.size(); + return header_map_.size(); } int HazelcastTestableLocalCache::bodyTestMapSize() { ASSERT(!unified_); - return bodyMap.size(); + return body_map_.size(); } int HazelcastTestableLocalCache::responseTestMapSize() { ASSERT(unified_); - return responseMap.size(); + return response_map_.size(); } void HazelcastTestableLocalCache::putTestResponse(uint64_t key, const HazelcastResponseEntry& entry) { checkConnection(); - responseMap[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); + response_map_[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); } void HazelcastTestableLocalCache::removeTestBody(uint64_t key, uint64_t order) { checkConnection(); - bodyMap.erase(orderedMapKey(key, order)); + body_map_.erase(orderedMapKey(key, order)); } LookupContextPtr HazelcastTestableLocalCache::makeLookupContext(LookupRequest&& request) { @@ -72,23 +72,23 @@ void HazelcastTestableLocalCache::updateHeaders(LookupContextPtr&& lookup_contex ASSERT(response_headers); } -CacheInfo HazelcastTestableLocalCache::cacheInfo() const { return CacheInfo(); } +CacheInfo HazelcastTestableLocalCache::cacheInfo() const { return {}; } void HazelcastTestableLocalCache::putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) { checkConnection(); - headerMap[mapKey(key)] = HazelcastHeaderPtr(new HazelcastHeaderEntry(entry)); + header_map_[mapKey(key)] = HazelcastHeaderPtr(new HazelcastHeaderEntry(entry)); } void HazelcastTestableLocalCache::putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) { checkConnection(); - bodyMap[orderedMapKey(key, order)] = HazelcastBodyPtr(new HazelcastBodyEntry(entry)); + body_map_[orderedMapKey(key, order)] = HazelcastBodyPtr(new HazelcastBodyEntry(entry)); } HazelcastHeaderPtr HazelcastTestableLocalCache::getHeader(const uint64_t key) { checkConnection(); - auto result = headerMap.find(mapKey(key)); - if (result != headerMap.end()) { + auto result = header_map_.find(mapKey(key)); + if (result != header_map_.end()) { // New objects are created during deserialization. Hence not returning the original one here. return HazelcastHeaderPtr(new HazelcastHeaderEntry(*result->second)); } else { @@ -98,8 +98,8 @@ HazelcastHeaderPtr HazelcastTestableLocalCache::getHeader(const uint64_t key) { HazelcastBodyPtr HazelcastTestableLocalCache::getBody(const uint64_t key, const uint64_t order) { checkConnection(); - auto result = bodyMap.find(orderedMapKey(key, order)); - if (result != bodyMap.end()) { + auto result = body_map_.find(orderedMapKey(key, order)); + if (result != body_map_.end()) { return HazelcastBodyPtr(new HazelcastBodyEntry(*result->second)); } else { return nullptr; @@ -117,9 +117,9 @@ void HazelcastTestableLocalCache::onMissingBody(uint64_t key, int32_t version, u } int body_count = body_size / body_partition_size_; while (body_count >= 0) { - bodyMap.erase(orderedMapKey(key, body_count--)); + body_map_.erase(orderedMapKey(key, body_count--)); } - headerMap.erase(mapKey(key)); + header_map_.erase(mapKey(key)); unlock(key); } void HazelcastTestableLocalCache::onVersionMismatch(uint64_t key, int32_t version, @@ -130,16 +130,16 @@ void HazelcastTestableLocalCache::onVersionMismatch(uint64_t key, int32_t versio void HazelcastTestableLocalCache::putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) { checkConnection(); - if (responseMap.find(mapKey(key)) != responseMap.end()) { + if (response_map_.find(mapKey(key)) != response_map_.end()) { return; } - responseMap[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); + response_map_[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); } HazelcastResponsePtr HazelcastTestableLocalCache::getResponse(const uint64_t key) { checkConnection(); - auto result = responseMap.find(mapKey(key)); - if (result != responseMap.end()) { + auto result = response_map_.find(mapKey(key)); + if (result != response_map_.end()) { return HazelcastResponsePtr(new HazelcastResponseEntry(*result->second)); } else { return nullptr; @@ -149,19 +149,20 @@ HazelcastResponsePtr HazelcastTestableLocalCache::getResponse(const uint64_t key bool HazelcastTestableLocalCache::tryLock(const uint64_t key) { checkConnection(); if (unified_) { - bool locked = std::find(responseLocks.begin(), responseLocks.end(), key) != responseLocks.end(); + bool locked = + std::find(response_locks_.begin(), response_locks_.end(), key) != response_locks_.end(); if (locked) { return false; } else { - responseLocks.push_back(key); + response_locks_.push_back(key); return true; } } else { - bool locked = std::find(headerLocks.begin(), headerLocks.end(), key) != headerLocks.end(); + bool locked = std::find(header_locks_.begin(), header_locks_.end(), key) != header_locks_.end(); if (locked) { return false; } else { - headerLocks.push_back(key); + header_locks_.push_back(key); return true; } } @@ -170,10 +171,11 @@ bool HazelcastTestableLocalCache::tryLock(const uint64_t key) { void HazelcastTestableLocalCache::unlock(const uint64_t key) { checkConnection(); if (unified_) { - responseLocks.erase(std::remove(responseLocks.begin(), responseLocks.end(), key), - responseLocks.end()); + response_locks_.erase(std::remove(response_locks_.begin(), response_locks_.end(), key), + response_locks_.end()); } else { - headerLocks.erase(std::remove(headerLocks.begin(), headerLocks.end(), key), headerLocks.end()); + header_locks_.erase(std::remove(header_locks_.begin(), header_locks_.end(), key), + header_locks_.end()); } } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h index a5162f644db61..5c1e095c1bab1 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h @@ -82,12 +82,12 @@ class HazelcastTestableLocalCache : public HazelcastTestableHttpCache, public Ha } } - std::unordered_map headerMap; - std::unordered_map bodyMap; - std::unordered_map responseMap; + std::unordered_map header_map_; + std::unordered_map body_map_; + std::unordered_map response_map_; - std::vector headerLocks; - std::vector responseLocks; + std::vector header_locks_; + std::vector response_locks_; bool connected_ = false; uint32_t random_counter_ = 0; diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 8f1374205c5e6..ed417e5c5fbd8 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -14,7 +14,7 @@ namespace HazelcastHttpCache { * Tests for UNIFIED cache mode. */ class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { - void SetUp() { + void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(true); // To test the cache with a real Hazelcast instance, remote cache // must be used during tests. diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h index 74fda06905b64..3113f17c068f4 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -107,8 +107,9 @@ class HazelcastHttpCacheTestBase : public testing::Test { const absl::string_view response_body) { InsertContextPtr insert_context = cache_->base().makeInsertContext(move(lookup)); insert_context->insertHeaders(response_headers, response_body == nullptr); - if (response_body == nullptr) + if (response_body == nullptr) { return; + } insert_context->insertBody(Buffer::OwnedImpl(response_body), nullptr, true); } From 31c0b3c2e4108e830185ea84a3c96a881bf6a72a Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Fri, 17 Apr 2020 20:09:50 +0300 Subject: [PATCH 09/33] Apply upstream changes Signed-off-by: Enes Ozcan --- .../cache/hazelcast_http_cache/hazelcast_common_cache_test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 412e7f4766ebb..8c0265f6971c2 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -135,14 +135,14 @@ TEST_P(HazelcastHttpCacheTest, Miss) { TEST_P(HazelcastHttpCacheTest, Fresh) { insert("/", getResponseHeaders(), ""); - time_source_.sleep(std::chrono::seconds(3600)); + time_source_.advanceTimeWait(std::chrono::seconds(3600)); lookup("/"); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); } TEST_P(HazelcastHttpCacheTest, Stale) { insert("/", getResponseHeaders(), ""); - time_source_.sleep(std::chrono::seconds(3601)); + time_source_.advanceTimeWait(std::chrono::seconds(3601)); lookup("/"); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); } From cabb409cf1962c9643cd077f799f95c74d514a49 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Fri, 8 May 2020 17:05:36 +0300 Subject: [PATCH 10/33] Disable PartitionAware for body entries. Signed-off-by: Enes Ozcan --- .../hazelcast_http_cache/hazelcast_cache_entry.cc | 9 +++------ .../hazelcast_http_cache/hazelcast_cache_entry.h | 15 ++------------- .../hazelcast_http_cache/hazelcast_context.cc | 5 ++--- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc index db0588fb5971f..26d88a32f4caf 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc @@ -98,19 +98,16 @@ void HazelcastBodyEntry::readUnifiedData(ObjectDataInput& reader) { HazelcastBodyEntry::HazelcastBodyEntry() = default; -HazelcastBodyEntry::HazelcastBodyEntry(int64_t header_key, std::vector&& buffer, - int32_t version) - : header_key_(header_key), version_(version), body_buffer_(std::move(buffer)) {} +HazelcastBodyEntry::HazelcastBodyEntry(std::vector&& buffer, int32_t version) + : version_(version), body_buffer_(std::move(buffer)) {} HazelcastBodyEntry::HazelcastBodyEntry(const HazelcastBodyEntry& other) { body_buffer_ = other.body_buffer_; - header_key_ = other.header_key_; version_ = other.version_; } HazelcastBodyEntry::HazelcastBodyEntry(HazelcastBodyEntry&& other) noexcept - : header_key_(other.header_key_), version_(other.version_), - body_buffer_(std::move(other.body_buffer_)) {} + : version_(other.version_), body_buffer_(std::move(other.body_buffer_)) {} void HazelcastResponseEntry::writeData(ObjectDataOutput& writer) const { response_header_.writeUnifiedData(writer); diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h index eb09c5a67a3e4..c4b980e9e54c5 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h @@ -5,7 +5,6 @@ #include "source/extensions/filters/http/cache/key.pb.h" #include "hazelcast/client/EntryView.h" -#include "hazelcast/client/PartitionAware.h" #include "hazelcast/client/serialization/ObjectDataOutput.h" namespace Envoy { @@ -14,7 +13,6 @@ namespace HttpFilters { namespace Cache { namespace HazelcastHttpCache { -using hazelcast::client::PartitionAware; using hazelcast::client::serialization::DataSerializableFactory; using hazelcast::client::serialization::IdentifiedDataSerializable; using hazelcast::client::serialization::ObjectDataInput; @@ -88,10 +86,10 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { * necessary partitions according to the request will be fetched from distributed map, * not the whole response. */ -class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAware { +class HazelcastBodyEntry : public IdentifiedDataSerializable { public: HazelcastBodyEntry(); - HazelcastBodyEntry(int64_t header_key, std::vector&& buffer, int32_t version); + HazelcastBodyEntry(std::vector&& buffer, int32_t version); HazelcastBodyEntry(const HazelcastBodyEntry& other); HazelcastBodyEntry(HazelcastBodyEntry&& other) noexcept; @@ -109,7 +107,6 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAw int32_t version() const { return version_; } void bodyBuffer(std::vector&& buffer) { body_buffer_ = std::move(buffer); } - void headerKey(int64_t key) { header_key_ = key; } void version(int32_t version) { version_ = version; } private: @@ -117,14 +114,6 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable, public PartitionAw int getClassId() const override { return HAZELCAST_BODY_TYPE_ID; } int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } - // hazelcast::client::PartitionAware - const int64_t* getPartitionKey() const override { return &header_key_; } - - /** The same hash key with the corresponding header. */ - // Not stored in distributed map but used to store related bodies in the same - // partition in Hazelcast cluster. - int64_t header_key_; - /** Derived from header. */ int32_t version_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index 70273aec19eea..6aa1ba7e9e8a9 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -202,7 +202,7 @@ void UnifiedInsertContext::insertResponse() { // Versions are not necessary for unified entries. Hence passing arbitrary 0 here. HazelcastHeaderEntry header(std::move(header_map_), std::move(variant_key_), buffer_vector_.size(), 0); - HazelcastBodyEntry body(variant_hash_key_, std::move(buffer_vector_), 0); + HazelcastBodyEntry body(std::move(buffer_vector_), 0); HazelcastResponseEntry entry(std::move(header), std::move(body)); try { @@ -444,8 +444,7 @@ bool DividedInsertContext::flushBuffer() { return true; } total_body_size_ += buffer_vector_.size(); - HazelcastBodyEntry bodyEntry(hz_cache_.mapKey(variant_hash_key_), std::move(buffer_vector_), - version_); + HazelcastBodyEntry bodyEntry(std::move(buffer_vector_), version_); buffer_vector_.clear(); try { hz_cache_.putBody(variant_hash_key_, body_order_++, bodyEntry); From 73347099ef7014f0e0744a99c3d24229bcac2aae Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Fri, 8 May 2020 22:43:25 +0300 Subject: [PATCH 11/33] Fix format and revert LookupRequest changes back. Signed-off-by: Enes Ozcan --- bazel/repositories.bzl | 2 +- bazel/repository_locations.bzl | 2 ++ .../extensions/filters/http/cache/http_cache.h | 18 ++++++++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index 2604184bf6d6d..de9cc7ecf5046 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -325,7 +325,7 @@ def _com_github_google_libprotobuf_mutator(): ) def _com_github_hazelcast_cpp_client(): - location = REPOSITORY_LOCATIONS["com_github_hazelcast_cpp_client"] + location = _get_location("com_github_hazelcast_cpp_client") http_archive( name = "com_github_hazelcast_cpp_client", build_file_content = BUILD_ALL_CONTENT, diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index da6d18c8574ee..9ba01a0b0978c 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -193,6 +193,8 @@ DEPENDENCY_REPOSITORIES = dict( # Using non official tarball due to missing submodule files in the official release. # TODO(enozcan): Use official release with init & updating submodules urls = ["https://github.com/enozcan/envoy-hazelcast-cpp-client/raw/master/hazelcast-cpp-client-3.12.1.zip"], + use_category = ["dataplane"], + cpe = "N/A", ), com_github_luajit_luajit = dict( sha256 = "409f7fe570d3c16558e594421c47bdd130238323c9d6fd6c83dedd2aaeb082a8", diff --git a/source/extensions/filters/http/cache/http_cache.h b/source/extensions/filters/http/cache/http_cache.h index de3dde3120f48..05e07a84fd7cb 100644 --- a/source/extensions/filters/http/cache/http_cache.h +++ b/source/extensions/filters/http/cache/http_cache.h @@ -165,6 +165,18 @@ class LookupRequest { // 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_; } + Key& key() { return key_; } + + // Returns the subset of this request's headers that are listed in + // envoy::extensions::filters::http::cache::v3alpha::CacheConfig::allowed_vary_headers. If a cache + // storage implementation forwards lookup requests to a remote cache server that supports *vary* + // headers, that server may need to see these headers. For local implementations, it may be + // simpler to instead call makeLookupResult with each potential response. + HeaderVector& vary_headers() { return vary_headers_; } + const HeaderVector& vary_headers() const { return vary_headers_; } + + // Time when this LookupRequest was created (in response to an HTTP request). + SystemTime timestamp() const { return timestamp_; } // WARNING: Incomplete--do not use in production (yet). // Returns a LookupResult suitable for sending to the cache filter's @@ -183,13 +195,7 @@ class LookupRequest { Key key_; std::vector request_range_spec_; - // Time when this LookupRequest was created (in response to an HTTP request). SystemTime timestamp_; - // The subset of this request's headers that are listed in - // envoy::extensions::filters::http::cache::v3alpha::CacheConfig::allowed_vary_headers. If a cache - // storage implementation forwards lookup requests to a remote cache server that supports *vary* - // headers, that server may need to see these headers. For local implementations, it may be - // simpler to instead call makeLookupResult with each potential response. HeaderVector vary_headers_; const std::string request_cache_control_; }; From 839c418c157952707ad4e70f06fb9576709b540a Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Mon, 11 May 2020 12:07:33 +0300 Subject: [PATCH 12/33] Add serialization tests. Signed-off-by: Enes Ozcan --- .../hazelcast_http_cache/hazelcast_cache.h | 2 +- .../hazelcast_cache_entry.cc | 20 ++++ .../hazelcast_cache_entry.h | 36 +++--- .../hazelcast_http_cache/hazelcast_context.cc | 28 ++--- .../hazelcast_http_cache_impl.cc | 8 +- .../filters/http/cache/http_cache.h | 4 +- .../http/cache/hazelcast_http_cache/BUILD | 9 ++ .../serialization_test.cc | 103 ++++++++++++++++++ 8 files changed, 173 insertions(+), 37 deletions(-) create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/serialization_test.cc diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h index 1c8f2366c250d..cd97afb39cc64 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h @@ -178,7 +178,7 @@ class HazelcastCache : public HttpCache { return std::to_string(key).append("#").append(std::to_string(order)); } - virtual ~HazelcastCache() = default; + ~HazelcastCache() override = default; protected: /** Cache mode */ diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc index 26d88a32f4caf..e6876974441e4 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc @@ -1,5 +1,7 @@ #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" +#include "common/protobuf/utility.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -78,6 +80,12 @@ HazelcastHeaderEntry::HazelcastHeaderEntry(HazelcastHeaderEntry&& other) noexcep : header_map_(std::move(other.header_map_)), variant_key_(std::move(other.variant_key_)), body_size_(other.body_size_), version_(other.version_) {} +bool HazelcastHeaderEntry::operator==(const HazelcastHeaderEntry& other) const { + return body_size_ == other.body_size_ && version_ == other.version_ && + Envoy::Protobuf::util::MessageDifferencer::Equals(variant_key_, other.variant_key_) && + *header_map_ == *other.header_map_; +} + void HazelcastBodyEntry::writeData(ObjectDataOutput& writer) const { writeUnifiedData(writer); writer.writeInt(version_); @@ -109,6 +117,10 @@ HazelcastBodyEntry::HazelcastBodyEntry(const HazelcastBodyEntry& other) { HazelcastBodyEntry::HazelcastBodyEntry(HazelcastBodyEntry&& other) noexcept : version_(other.version_), body_buffer_(std::move(other.body_buffer_)) {} +bool HazelcastBodyEntry::operator==(const HazelcastBodyEntry& other) const { + return version_ == other.version_ && body_buffer_ == other.body_buffer_; +} + void HazelcastResponseEntry::writeData(ObjectDataOutput& writer) const { response_header_.writeUnifiedData(writer); response_body_.writeUnifiedData(writer); @@ -125,6 +137,14 @@ HazelcastResponseEntry::HazelcastResponseEntry(HazelcastHeaderEntry&& header, HazelcastBodyEntry&& body) : response_header_(std::move(header)), response_body_(std::move(body)){}; +bool HazelcastResponseEntry::operator==(const HazelcastResponseEntry& other) const { + // Ignore the fields not written to distributed map. + return response_body_.buffer() == other.body().buffer() && + Envoy::Protobuf::util::MessageDifferencer::Equals(response_header_.variantKey(), + other.header().variantKey()) && + *response_header_.headerMap() == *other.header().headerMap(); +} + } // namespace HazelcastHttpCache } // namespace Cache } // namespace HttpFilters diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h index c4b980e9e54c5..2dfca97bc5d34 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h @@ -42,26 +42,26 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { // hazelcast::client::serialization::IdentifiedDataSerializable void writeData(ObjectDataOutput& writer) const override; void readData(ObjectDataInput& reader) override; + int getClassId() const override { return HAZELCAST_HEADER_TYPE_ID; } + int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } // Only required fields of a header entry for unified mode are // de/serialized in unifiedData methods. void writeUnifiedData(ObjectDataOutput& writer) const; void readUnifiedData(ObjectDataInput& reader); + Http::ResponseHeaderMapPtr& headerMap() { return header_map_; } + const Http::ResponseHeaderMapPtr& headerMap() const { return header_map_; } const Key& variantKey() const { return variant_key_; } uint64_t bodySize() const { return body_size_; } int32_t version() const { return version_; } - Http::ResponseHeaderMapPtr& headerMap() { return header_map_; } void variantKey(Key&& key) { variant_key_ = std::move(key); } - void headerMap(Http::ResponseHeaderMapPtr&& header_map) { header_map_ = std::move(header_map); } - void bodySize(uint64_t body_size) { body_size_ = body_size; } + void version(int32_t version) { version_ = version; } -private: - // hazelcast::client::serialization::IdentifiedDataSerializable - int getClassId() const override { return HAZELCAST_HEADER_TYPE_ID; } - int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + bool operator==(const HazelcastHeaderEntry& other) const; +private: Http::ResponseHeaderMapPtr header_map_; /** Key generated by the cache filter and modified with vary headers later on. */ @@ -96,6 +96,8 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable { // hazelcast::client::serialization::IdentifiedDataSerializable void writeData(ObjectDataOutput& writer) const override; void readData(ObjectDataInput& reader) override; + int getClassId() const override { return HAZELCAST_BODY_TYPE_ID; } + int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } // Only required fields of a body entry for unified mode are // de/serialized in unifiedData methods. @@ -105,15 +107,13 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable { size_t length() const { return body_buffer_.size(); } hazelcast::byte* begin() { return body_buffer_.data(); } int32_t version() const { return version_; } + const std::vector& buffer() const { return body_buffer_; } - void bodyBuffer(std::vector&& buffer) { body_buffer_ = std::move(buffer); } void version(int32_t version) { version_ = version; } -private: - // hazelcast::client::serialization::IdentifiedDataSerializable - int getClassId() const override { return HAZELCAST_BODY_TYPE_ID; } - int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + bool operator==(const HazelcastBodyEntry& other) const; +private: /** Derived from header. */ int32_t version_; @@ -133,16 +133,20 @@ class HazelcastResponseEntry : public IdentifiedDataSerializable { HazelcastResponseEntry(); HazelcastResponseEntry(HazelcastHeaderEntry&& header, HazelcastBodyEntry&& body); - HazelcastHeaderEntry& header() { return response_header_; } - HazelcastBodyEntry& body() { return response_body_; } - -private: // hazelcast::client::serialization::IdentifiedDataSerializable void writeData(ObjectDataOutput& writer) const override; void readData(ObjectDataInput& reader) override; int getClassId() const override { return HAZELCAST_RESPONSE_TYPE_ID; } int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } + HazelcastHeaderEntry& header() { return response_header_; } + HazelcastBodyEntry& body() { return response_body_; } + const HazelcastHeaderEntry& header() const { return response_header_; } + const HazelcastBodyEntry& body() const { return response_body_; } + + bool operator==(const HazelcastResponseEntry& other) const; + +private: HazelcastHeaderEntry response_header_; HazelcastBodyEntry response_body_; }; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index 6aa1ba7e9e8a9..e5aed80d83394 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -39,12 +39,12 @@ void HazelcastLookupContextBase::handleLookupFailure(absl::string_view message, void HazelcastLookupContextBase::createVariantKey(Key& raw_key) { ASSERT(raw_key.custom_fields_size() == 0); ASSERT(raw_key.custom_ints_size() == 0); // Key must be pure. - if (lookup_request_.vary_headers().empty()) { + if (lookup_request_.varyHeaders().empty()) { return; } std::vector> header_strings; - for (const Http::HeaderEntry& header : lookup_request_.vary_headers()) { + for (const Http::HeaderEntry& header : lookup_request_.varyHeaders()) { header_strings.push_back(std::make_pair(std::string(header.key().getStringView()), std::string(header.value().getStringView()))); } @@ -101,12 +101,12 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { ENVOY_LOG(debug, "Looking up unified response with key: {}u", variant_hash_key_); try { response_ = hz_cache_.getResponse(variant_hash_key_); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " "insertions until the connection is restored...", cb); return; - } catch (OperationTimeoutException e) { + } catch (OperationTimeoutException& e) { handleLookupFailure("Operation timed out during cache lookup.", cb); return; } catch (std::exception& e) { @@ -207,9 +207,9 @@ void UnifiedInsertContext::insertResponse() { HazelcastResponseEntry entry(std::move(header), std::move(body)); try { hz_cache_.putResponseIfAbsent(variant_hash_key_, entry); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Failed to insert response."); - } catch (OperationTimeoutException e) { + } catch (OperationTimeoutException& e) { ENVOY_LOG(warn, "Operation timed out during cache insertion."); } catch (std::exception& e) { ENVOY_LOG(warn, "Response insertion to cache has failed: {}", e.what()); @@ -225,12 +225,12 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { HazelcastHeaderPtr header_entry; try { header_entry = hz_cache_.getHeader(variant_hash_key_); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " "insertions until the connection is restored.", cb); return; - } catch (OperationTimeoutException e) { + } catch (OperationTimeoutException& e) { handleLookupFailure("Operation timed out during cache lookup.", cb); return; } catch (std::exception& e) { @@ -263,7 +263,7 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { // Hazelcast cluster. try { abort_insertion_ = !hz_cache_.tryLock(variant_hash_key_); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and insertions" " until the connection is restored...", cb); @@ -300,12 +300,12 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal body_index); try { body = hz_cache_.getBody(variant_hash_key_, body_index); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { handleBodyLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " "insertions until the connection is restored...", cb); return; - } catch (OperationTimeoutException e) { + } catch (OperationTimeoutException& e) { handleBodyLookupFailure("Operation timed out during cache lookup.", cb); return; } catch (std::exception& e) { @@ -448,10 +448,10 @@ bool DividedInsertContext::flushBuffer() { buffer_vector_.clear(); try { hz_cache_.putBody(variant_hash_key_, body_order_++, bodyEntry); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { ENVOY_LOG(warn, "Hazelcast cluster connection is lost!"); return false; - } catch (OperationTimeoutException e) { + } catch (OperationTimeoutException& e) { ENVOY_LOG(warn, "Operation timed out during body insertion."); return false; } catch (std::exception& e) { @@ -478,7 +478,7 @@ void DividedInsertContext::insertHeader() { hz_cache_.putHeader(variant_hash_key_, header); hz_cache_.unlock(variant_hash_key_); ENVOY_LOG(debug, "Inserted header entry with key {}u", variant_hash_key_); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { ENVOY_LOG(warn, "Hazelcast Connection is offline!"); // To handle leftover locks, hazelcast.lock.max.lease.time.seconds property must // be set to a reasonable value on the server side. It is Long.MAX by default. diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc index e89004efccb2c..1e8e63c5120e1 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc @@ -42,9 +42,9 @@ void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, } else { updateDividedHeaders(std::move(lookup_context), std::move(response_headers)); } - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { ENVOY_LOG(warn, "Hazelcast Connection is offline!"); - } catch (OperationTimeoutException e) { + } catch (OperationTimeoutException& e) { ENVOY_LOG(warn, "Updating headers has timed out."); } catch (std::exception& e) { ENVOY_LOG(warn, "Updating headers has failed: {}", e.what()); @@ -100,10 +100,10 @@ void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t b } getHeaderMap().remove(mapKey(key)); unlock(key); - } catch (HazelcastClientOfflineException e) { + } catch (HazelcastClientOfflineException& e) { // see DividedInsertContext#insertHeader() for left over locks on a connection failure. ENVOY_LOG(warn, "Hazelcast Connection is offline!"); - } catch (OperationTimeoutException e) { + } catch (OperationTimeoutException& e) { ENVOY_LOG(warn, "Clean up for missing body has timed out."); } catch (std::exception& e) { ENVOY_LOG(warn, "Clean up for missing body has failed: {}", e.what()); diff --git a/source/extensions/filters/http/cache/http_cache.h b/source/extensions/filters/http/cache/http_cache.h index 05e07a84fd7cb..761d66be732af 100644 --- a/source/extensions/filters/http/cache/http_cache.h +++ b/source/extensions/filters/http/cache/http_cache.h @@ -172,8 +172,8 @@ class LookupRequest { // storage implementation forwards lookup requests to a remote cache server that supports *vary* // headers, that server may need to see these headers. For local implementations, it may be // simpler to instead call makeLookupResult with each potential response. - HeaderVector& vary_headers() { return vary_headers_; } - const HeaderVector& vary_headers() const { return vary_headers_; } + HeaderVector& varyHeaders() { return vary_headers_; } + const HeaderVector& varyHeaders() const { return vary_headers_; } // Time when this LookupRequest was created (in response to an HTTP request). SystemTime timestamp() const { return timestamp_; } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 19ff62be93b7f..6c4c305276b81 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -45,6 +45,15 @@ envoy_extension_cc_test( ], ) +envoy_extension_cc_test( + name = "hazelcast_serialization_test", + srcs = ["serialization_test.cc"], + extension_name = "envoy.filters.http.cache.hazelcast_http_cache", + deps = [ + ":hazelcast_test_lib", + ], +) + envoy_extension_cc_test_library( name = "hazelcast_test_lib", srcs = ["hazelcast_test_cache.cc"], diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/serialization_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/serialization_test.cc new file mode 100644 index 0000000000000..ab6ddfd8a8c57 --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/serialization_test.cc @@ -0,0 +1,103 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" + +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "hazelcast/client/HazelcastAll.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +using hazelcast::client::serialization::ObjectDataInput; +using hazelcast::client::serialization::ObjectDataOutput; +using hazelcast::client::serialization::pimpl::DataInput; +using hazelcast::client::serialization::pimpl::DataOutput; + +class SerializationTest : public testing::Test { +protected: + void SetUp() override { null_serializer = nullptr; } + + template std::vector serialize(const T& deserialized) { + DataOutput data_output; + ObjectDataOutput object_data_output(data_output, null_serializer); + deserialized.writeData(object_data_output); + return *object_data_output.toByteArray(); + } + + template T deserialize(const std::vector& serialized) { + T object; + DataInput data_input(serialized); + // the behavior of (*null_serializer) here has no effect on tests. + ObjectDataInput objectDataInput(data_input, *null_serializer); + object.readData(objectDataInput); + return object; + } + + HazelcastHeaderEntry createTestHeader() { + auto headers = Http::ResponseHeaderMapPtr{ + new Http::TestResponseHeaderMapImpl{{"cache-control", "public, max-age=3600"}}}; + Key key; + key.set_cluster_name("some_cluster"); + key.set_host("some_host"); + key.set_path("some_path"); + key.set_query("some_query"); + key.set_clear_http(true); + return HazelcastHeaderEntry(std::move(headers), std::move(key), 2008, 2020); + } + + HazelcastBodyEntry createTestBody() { + return HazelcastBodyEntry( + std::vector({'h', 'a', 'z', 'e', 'l', 'c', 'a', 's', 't'}), 2008); + } + + HazelcastResponseEntry createTestResponse() { + return HazelcastResponseEntry(createTestHeader(), createTestBody()); + } + + SerializerHolder* null_serializer; +}; + +TEST_F(SerializationTest, HeaderEntry) { + HazelcastHeaderEntry original_header = createTestHeader(); + auto serialized = serialize(original_header); + HazelcastHeaderEntry new_header = deserialize(serialized); + EXPECT_EQ(original_header, new_header); +} + +TEST_F(SerializationTest, BodyEntry) { + HazelcastBodyEntry original_body = createTestBody(); + auto serialized = serialize(original_body); + HazelcastBodyEntry new_body = deserialize(serialized); + EXPECT_EQ(original_body, new_body); +} + +TEST_F(SerializationTest, ResponseEntry) { + HazelcastResponseEntry original_response = createTestResponse(); + auto serialized = serialize(original_response); + HazelcastResponseEntry new_response = deserialize(serialized); + EXPECT_EQ(original_response, new_response); +} + +TEST_F(SerializationTest, SerializerId) { + HazelcastHeaderEntry header; + HazelcastBodyEntry body; + HazelcastResponseEntry response; + + EXPECT_EQ(header.getClassId(), HAZELCAST_HEADER_TYPE_ID); + EXPECT_EQ(body.getClassId(), HAZELCAST_BODY_TYPE_ID); + EXPECT_EQ(response.getClassId(), HAZELCAST_RESPONSE_TYPE_ID); + + EXPECT_EQ(header.getFactoryId(), HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID); + EXPECT_EQ(body.getFactoryId(), HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID); + EXPECT_EQ(response.getFactoryId(), HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID); +} + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy From 2576ebc93fd63a6089ad62cda24890b287fdd4cf Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Wed, 20 May 2020 10:23:51 +0300 Subject: [PATCH 13/33] Move remote calls to cache accessors. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_http_cache/BUILD | 7 +- .../hazelcast_cache_entry.h | 18 +- .../hazelcast_http_cache/hazelcast_context.cc | 60 ++-- .../hazelcast_http_cache/hazelcast_context.h | 48 ++-- .../hazelcast_http_cache.cc | 184 ++++++++++++ ...zelcast_cache.h => hazelcast_http_cache.h} | 149 +++++++--- .../hazelcast_http_cache_impl.cc | 267 ------------------ .../hazelcast_http_cache_impl.h | 120 -------- .../hazelcast_storage_accessor.cc | 108 +++++++ .../hazelcast_storage_accessor.h | 128 +++++++++ .../http/cache/hazelcast_http_cache/util.h | 4 +- .../http/cache/hazelcast_http_cache/BUILD | 26 +- ...test.cc => hazelcast_cache_config_test.cc} | 1 + .../hazelcast_common_cache_test.cc | 78 +++-- .../hazelcast_divided_cache_test.cc | 248 +++++++++++++--- ... => hazelcast_entry_serialization_test.cc} | 0 .../hazelcast_test_cache.cc | 188 ------------ .../hazelcast_test_cache.h | 140 --------- .../hazelcast_unified_cache_test.cc | 50 +++- .../hazelcast_http_cache/test_accessors.h | 248 ++++++++++++++++ .../cache/hazelcast_http_cache/test_caches.h | 80 ++++++ .../{util.h => test_util.h} | 8 +- 22 files changed, 1242 insertions(+), 918 deletions(-) create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc rename source/extensions/filters/http/cache/hazelcast_http_cache/{hazelcast_cache.h => hazelcast_http_cache.h} (57%) delete mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc delete mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc create mode 100644 source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h rename test/extensions/filters/http/cache/hazelcast_http_cache/{config_utils_test.cc => hazelcast_cache_config_test.cc} (98%) rename test/extensions/filters/http/cache/hazelcast_http_cache/{serialization_test.cc => hazelcast_entry_serialization_test.cc} (100%) delete mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc delete mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h create mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h rename test/extensions/filters/http/cache/hazelcast_http_cache/{util.h => test_util.h} (95%) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD index be9bf17b15da0..a7e998d39aa23 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -14,13 +14,14 @@ envoy_cc_extension( srcs = [ "hazelcast_cache_entry.cc", "hazelcast_context.cc", - "hazelcast_http_cache_impl.cc", + "hazelcast_http_cache.cc", + "hazelcast_storage_accessor.cc", ], hdrs = [ - "hazelcast_cache.h", "hazelcast_cache_entry.h", "hazelcast_context.h", - "hazelcast_http_cache_impl.h", + "hazelcast_http_cache.h", + "hazelcast_storage_accessor.h", "util.h", ], external_deps = ["hazelcast_cpp_client"], diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h index 2dfca97bc5d34..f07b4b70c4bb7 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h @@ -28,7 +28,7 @@ static const int HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID = 1000; * * @note In DIVIDED cache mode, response headers and corresponding bodies will be * stored in different distributed maps. This option is in favor of the efficiency - * of range http requests. For each header entry, there will be body entries (if any) + * of range HTTP requests. For each header entry, there will be body entries (if any) * with a relevant key. */ class HazelcastHeaderEntry : public IdentifiedDataSerializable { @@ -45,8 +45,8 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { int getClassId() const override { return HAZELCAST_HEADER_TYPE_ID; } int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } - // Only required fields of a header entry for unified mode are - // de/serialized in unifiedData methods. + // Only required fields of a header entry for unified mode are de/serialized + // in unifiedData methods. void writeUnifiedData(ObjectDataOutput& writer) const; void readUnifiedData(ObjectDataInput& reader); @@ -64,7 +64,7 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { private: Http::ResponseHeaderMapPtr header_map_; - /** Key generated by the cache filter and modified with vary headers later on. */ + /** The key generated by the cache filter and modified with vary headers later on. */ Key variant_key_; /** Total body size of the response with these headers. */ @@ -80,8 +80,8 @@ class HazelcastHeaderEntry : public IdentifiedDataSerializable { * Response body wrapper for cache entries. * * @note In DIVIDED cache mode, response headers and corresponding bodies will be stored in - * different distributed maps. For a response HeaderEntry with 64 bit hash key , bodies - * will be stored with keys "#0", "#1", "#2".. and so on in a contiguous manner. + * different distributed maps. For a response HeaderEntry with 64-bit hash key , bodies + * will be stored with keys "#0", "#1", "#2".. and so on in a continuous manner. * Body partition size is fixed and configurable via cache config. On a range request, only * necessary partitions according to the request will be fetched from distributed map, * not the whole response. @@ -99,8 +99,8 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable { int getClassId() const override { return HAZELCAST_BODY_TYPE_ID; } int getFactoryId() const override { return HAZELCAST_ENTRY_SERIALIZER_FACTORY_ID; } - // Only required fields of a body entry for unified mode are - // de/serialized in unifiedData methods. + // Only required fields of a header entry for unified mode are de/serialized + // in unifiedData methods. void writeUnifiedData(ObjectDataOutput& writer) const; void readUnifiedData(ObjectDataInput& reader); @@ -114,7 +114,7 @@ class HazelcastBodyEntry : public IdentifiedDataSerializable { bool operator==(const HazelcastBodyEntry& other) const; private: - /** Derived from header. */ + /** Derived from header */ int32_t version_; std::vector body_buffer_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index e5aed80d83394..cab19d4d75772 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -12,15 +12,15 @@ namespace HazelcastHttpCache { using Envoy::Protobuf::util::MessageDifferencer; -HazelcastLookupContextBase::HazelcastLookupContextBase(HazelcastCache& cache, +HazelcastLookupContextBase::HazelcastLookupContextBase(HazelcastHttpCache& cache, LookupRequest&& request) : hz_cache_(cache), lookup_request_(std::move(request)) { createVariantKey(lookup_request_.key()); variant_hash_key_ = stableHashKey(lookup_request_.key()); } +// TODO(enozcan): Support trailers when implemented on the filter side. void HazelcastLookupContextBase::getTrailers(LookupTrailersCallback&&) { - // TODO(enozcan): Support trailers when implemented on the filter side. NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } @@ -43,12 +43,16 @@ void HazelcastLookupContextBase::createVariantKey(Key& raw_key) { return; } std::vector> header_strings; - for (const Http::HeaderEntry& header : lookup_request_.varyHeaders()) { header_strings.push_back(std::make_pair(std::string(header.key().getStringView()), std::string(header.value().getStringView()))); } + arrangeVariantHeaders(raw_key, header_strings); +} +// Decoupled from HazelcastLookupContextBase::createVariantKey to be able to test. +void HazelcastLookupContextBase::arrangeVariantHeaders( + Key& raw_key, std::vector>& header_strings) { // Different order of headers causes different hash keys even if their both key and value // are the same. That is, the following two header lists will cause different hashes for // the same response and hence they are sorted before insertion. @@ -64,16 +68,15 @@ void HazelcastLookupContextBase::createVariantKey(Key& raw_key) { return left.first == right.first ? false : left.first < right.first; }); - // stableHashKey now creates variant hash for the key since its custom_fields are like: + // stableHashKey will create the same variant hashes for the above keys since both + // have the same custom_fields: // [ "Accept-Encoding", "gzip", "User-Agent", "desktop"] for (auto& header : header_strings) { raw_key.add_custom_fields(std::move(header.first)); raw_key.add_custom_fields(std::move(header.second)); } - // TODO(enozcan): Ensure the generation of the same key for the same response independent // from the header orders. - // // Different hash keys will be created if the order of values differ for the same // vary header key. The response will not be affected but the same response will // be cached with different keys. i.e. two different hashes exist for the followings @@ -83,18 +86,18 @@ void HazelcastLookupContextBase::createVariantKey(Key& raw_key) { } HazelcastInsertContextBase::HazelcastInsertContextBase(LookupContext& lookup_context, - HazelcastCache& cache) + HazelcastHttpCache& cache) : hz_cache_(cache), max_body_size_(cache.maxBodySize()), variant_hash_key_(static_cast(lookup_context).variantHashKey()), variant_key_(static_cast(lookup_context).variantKey()), abort_insertion_(static_cast(lookup_context).isAborted()) {} +// TODO(enozcan): Support trailers when implemented on the filter side. void HazelcastInsertContextBase::insertTrailers(const Http::ResponseTrailerMap&) { - // TODO(enozcan): Support trailers when implemented on the filter side. NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } -UnifiedLookupContext::UnifiedLookupContext(HazelcastCache& cache, LookupRequest&& request) +UnifiedLookupContext::UnifiedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request) : HazelcastLookupContextBase(cache, std::move(request)) {} void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { @@ -102,7 +105,7 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { try { response_ = hz_cache_.getResponse(variant_hash_key_); } catch (HazelcastClientOfflineException& e) { - handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " + handleLookupFailure("Hazelcast cluster connection is lost! Aborting all lookups and " "insertions until the connection is restored...", cb); return; @@ -148,7 +151,7 @@ void UnifiedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal cb(std::make_unique(data, range.length())); } -UnifiedInsertContext::UnifiedInsertContext(LookupContext& lookup_context, HazelcastCache& cache) +UnifiedInsertContext::UnifiedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache) : HazelcastInsertContextBase(lookup_context, cache) {} void UnifiedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response_headers, @@ -178,7 +181,7 @@ void UnifiedInsertContext::insertBody(const Buffer::Instance& chunk, buffer_vector_.resize(buffer_length + chunk.length()); chunk.copyOut(0, chunk.length(), buffer_vector_.data() + buffer_length); } else { - // Store the body copied until now and abort the further attempted. + // Store the body copied until now and abort the further attempts. buffer_vector_.resize(max_body_size_); chunk.copyOut(0, allowed_size, buffer_vector_.data() + buffer_length); insertResponse(); @@ -216,7 +219,7 @@ void UnifiedInsertContext::insertResponse() { } } -DividedLookupContext::DividedLookupContext(HazelcastCache& cache, LookupRequest&& request) +DividedLookupContext::DividedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request) : HazelcastLookupContextBase(cache, std::move(request)), body_partition_size_(cache.bodySizePerEntry()){}; @@ -245,8 +248,6 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { handleLookupFailure("Mismatched keys found for unsigned hash: " + std::to_string(variant_hash_key_), cb, false); - // Unsigned hash is denoted here since entries are stored with signed keys correspond - // to the unsigned ones. This is because of Hazelcast behavior. return; } this->total_body_size_ = header_entry->bodySize(); @@ -255,19 +256,22 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { cb(lookup_request_.makeLookupResult(std::move(header_entry->headerMap()), total_body_size_)); } else { ENVOY_LOG(debug, "Missed divided response lookup for key: {}u", variant_hash_key_); - // To prevent multiple insertion contexts to create the same response in the cache, - // mark only one of them responsible for the insertion using Hazelcast map key locks. - // If key is not locked, it will be acquired here and only one insertion context - // created for this lookup will be responsible for the insertion. This is also valid - // when multiple cache filters from different proxies are connected to the same - // Hazelcast cluster. try { + // To prevent multiple insertion contexts to create the same response in the cache, + // mark only one of them responsible for the insertion using Hazelcast map key locks. + // If key is not locked, it will be acquired here and only one insertion context + // created for this lookup will be responsible for the insertion. This is also valid + // when multiple cache filters from different proxies are connected to the same + // Hazelcast cluster. abort_insertion_ = !hz_cache_.tryLock(variant_hash_key_); } catch (HazelcastClientOfflineException& e) { handleLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and insertions" " until the connection is restored...", cb); return; + } catch (OperationTimeoutException& e) { + handleLookupFailure("Operation timed out during tryLock!", cb); + return; } catch (std::exception& e) { handleLookupFailure(fmt::format("Lock trial has failed: {}", e.what()), cb); return; @@ -357,7 +361,7 @@ void DividedLookupContext::handleBodyLookupFailure(absl::string_view message, cb(nullptr); } -DividedInsertContext::DividedInsertContext(LookupContext& lookup_context, HazelcastCache& cache) +DividedInsertContext::DividedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache) : HazelcastInsertContextBase(lookup_context, cache), body_partition_size_(cache.bodySizePerEntry()), version_(createVersion()) {} @@ -478,14 +482,14 @@ void DividedInsertContext::insertHeader() { hz_cache_.putHeader(variant_hash_key_, header); hz_cache_.unlock(variant_hash_key_); ENVOY_LOG(debug, "Inserted header entry with key {}u", variant_hash_key_); - } catch (HazelcastClientOfflineException& e) { - ENVOY_LOG(warn, "Hazelcast Connection is offline!"); - // To handle leftover locks, hazelcast.lock.max.lease.time.seconds property must - // be set to a reasonable value on the server side. It is Long.MAX by default. + // To handle leftover locks in a failure, hazelcast.lock.max.lease.time.seconds property + // must be set to a reasonable value on the server side. It is Long.MAX by default. // To make this independent from the server configuration, tryLock with leaseTime // option can be used when available in a future release of cpp client. The related // issue can be tracked at: https://github.com/hazelcast/hazelcast-cpp-client/issues/579 // TODO(enozcan): Use tryLock with leaseTime when released for Hazelcast cpp client. + } catch (HazelcastClientOfflineException& e) { + ENVOY_LOG(warn, "Hazelcast Connection is offline!"); } catch (std::exception& e) { ENVOY_LOG(warn, "Failed to complete response insertion: {}", e.what()); } @@ -496,8 +500,8 @@ int32_t DividedInsertContext::createVersion() { // the versions of two different header entries with distinct // hash keys are the same, this will not cause a problem at all. // We only need a stamp for bodies which inserted for this context. - // Since this version is stored in cache entries, 32 bit random - // derived from the 64 bit is preferred here. + // Since this version is stored in cache entries, a 32-bit random + // derived from the 64-bit one is preferred here. // Range: from (int32.MIN + 1) to (int32.MAX - 1), inclusive. uint64_t rand64 = hz_cache_.random(); uint64_t max = std::numeric_limits::max(); diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h index 0ecf82a2d0b2f..cdae452dff79a 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h @@ -1,6 +1,6 @@ #pragma once -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" namespace Envoy { namespace Extensions { @@ -14,12 +14,11 @@ namespace HazelcastHttpCache { class HazelcastLookupContextBase : public LookupContext, public Logger::Loggable { public: - HazelcastLookupContextBase(HazelcastCache& cache, LookupRequest&& request); + HazelcastLookupContextBase(HazelcastHttpCache& cache, LookupRequest&& request); // LookupContext void getTrailers(LookupTrailersCallback&&) override; - const LookupRequest& request() const { return lookup_request_; } const Key& variantKey() const { return lookup_request_.key(); } uint64_t variantHashKey() const { return variant_hash_key_; } bool isAborted() const { return abort_insertion_; } @@ -28,16 +27,19 @@ class HazelcastLookupContextBase : public LookupContext, void handleLookupFailure(absl::string_view message, const LookupHeadersCallback& cb, bool warn_log = true); - HazelcastCache& hz_cache_; + HazelcastHttpCache& hz_cache_; LookupRequest lookup_request_; - /** Hash key aware of vary headers. Lookup will be performed using this. */ + /** Hash key aware of vary headers. Lookup to header and response entry is performed using this + * key. */ uint64_t variant_hash_key_; - /** Flag to notice insert context created with this lookup. */ + /** Flag to notice insert context created for this lookup */ bool abort_insertion_ = false; private: + friend class HazelcastHttpCacheTest; + /** * The keys created by the cache filter for lookups and inserts are not aware * of the vary headers of the request. Instead, cache filter expects a @@ -45,11 +47,21 @@ class HazelcastLookupContextBase : public LookupContext, * headers. Rather than storing multiple responses with the same key and * then querying them according to vary headers, a different key for each * response including vary headers in custom fields is created here. Hence - * responses can be found by their directly. + * responses can be found by their directly without querying. * - * @param raw_key Key created by the filter. + * @param raw_key Key to be modified created by the filter. */ void createVariantKey(Key& raw_key); + + /** + * Fills the custom_fields of a key with given headers in alphabetical order. + * + * @note Decoupled from createVariantKey for testing. + * @param raw_key Key to be modified created by the filter. + * @param headers Vary headers to be included in the key. + */ + void arrangeVariantHeaders(Key& raw_key, + std::vector>& headers); }; /** @@ -58,20 +70,20 @@ class HazelcastLookupContextBase : public LookupContext, class HazelcastInsertContextBase : public InsertContext, public Logger::Loggable { public: - HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastCache& cache); + HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastHttpCache& cache); // InsertContext void insertTrailers(const Http::ResponseTrailerMap&) override; protected: - HazelcastCache& hz_cache_; + HazelcastHttpCache& hz_cache_; - // From plugin configuration + // From HazelcastHttpCache configuration const uint64_t max_body_size_; bool committed_end_stream_ = false; - // From lookup context + // Derived from lookup context const uint64_t variant_hash_key_; Key variant_key_; const bool abort_insertion_; @@ -89,7 +101,7 @@ class HazelcastInsertContextBase : public InsertContext, */ class UnifiedLookupContext : public HazelcastLookupContextBase { public: - UnifiedLookupContext(HazelcastCache& cache, LookupRequest&& request); + UnifiedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request); void getHeaders(LookupHeadersCallback&& cb) override; void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; @@ -103,7 +115,7 @@ class UnifiedLookupContext : public HazelcastLookupContextBase { */ class UnifiedInsertContext : public HazelcastInsertContextBase { public: - UnifiedInsertContext(LookupContext& lookup_context, HazelcastCache& cache); + UnifiedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache); void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) override; void insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, bool end_stream) override; @@ -121,7 +133,7 @@ class UnifiedInsertContext : public HazelcastInsertContextBase { */ class DividedLookupContext : public HazelcastLookupContextBase { public: - DividedLookupContext(HazelcastCache& cache, LookupRequest&& request); + DividedLookupContext(HazelcastHttpCache& cache, LookupRequest&& request); void getHeaders(LookupHeadersCallback&& cb) override; void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override; @@ -134,7 +146,7 @@ class DividedLookupContext : public HazelcastLookupContextBase { int32_t version_; uint64_t total_body_size_; - /** Max body size per body entry defined via config. */ + /** Max body size per body entry defined via cache config. */ const uint64_t body_partition_size_; }; @@ -143,7 +155,7 @@ class DividedLookupContext : public HazelcastLookupContextBase { */ class DividedInsertContext : public HazelcastInsertContextBase { public: - DividedInsertContext(LookupContext& lookup_context, HazelcastCache& cache); + DividedInsertContext(LookupContext& lookup_context, HazelcastHttpCache& cache); void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) override; void insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, bool end_stream) override; @@ -152,7 +164,7 @@ class DividedInsertContext : public HazelcastInsertContextBase { /** * Copies bytes from source to local buffer. Insertion to the cache happens after * the local buffer is full or the end stream is committed by the filter. - * @param offset Byte offset for the source. Updated much after copy. + * @param offset Byte offset for the source. Updated much after copying. * @param size Number of bytes to be copied into the local buffer. * @param source Body content given by the filter. */ diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc new file mode 100644 index 0000000000000..ef2f880c11130 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -0,0 +1,184 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +using hazelcast::client::ClientConfig; +using hazelcast::client::exception::HazelcastClientOfflineException; +using hazelcast::client::exception::OperationTimeoutException; +using hazelcast::client::serialization::DataSerializableFactory; + +HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) + : unified_(config.unified()), + body_partition_size_(ConfigUtil::validPartitionSize(config.body_partition_size())), + max_body_size_(ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())), + cache_config_(config) {} + +void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { + try { + if (!tryLock(key)) { + // Let lock owner context to recover it. + return; + } + auto header = getHeader(key); + if (header && header->version() != version) { + // The missed body does not belong to the looked up header. Probably eviction and then + // insertion for the header has happened in the meantime. Since new insertion will + // override the existing bodies, ignore the cleanup and let orphan bodies (belong to + // evicted header, not overridden) be evicted by TTL as well. + unlock(key); + return; + } + int body_count = body_size / body_partition_size_; + while (body_count >= 0) { + accessor_->removeBodyAsync(orderedMapKey(key, body_count--)); + } + accessor_->removeHeader(mapKey(key)); + unlock(key); + } catch (HazelcastClientOfflineException& e) { + // see DividedInsertContext::insertHeader for left over locks on a connection failure. + ENVOY_LOG(warn, "Hazelcast Connection is offline!"); + } catch (std::exception& e) { + ENVOY_LOG(warn, "Clean up for missing body has failed: {}", e.what()); + } +} + +void HazelcastHttpCache::onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) { + onMissingBody(key, version, body_size); +} + +void HazelcastHttpCache::start() { + if (accessor_ && accessor_->isRunning()) { + ENVOY_LOG(warn, "Client is already connected. Cluster name: {}", accessor_->clusterName()); + return; + } + + ClientConfig client_config = ConfigUtil::getClientConfig(cache_config_); + client_config.getSerializationConfig().addDataSerializableFactory( + HazelcastCacheEntrySerializableFactory::FACTORY_ID, + boost::shared_ptr(new HazelcastCacheEntrySerializableFactory())); + + if (!accessor_) { + accessor_ = std::make_unique( + std::move(client_config), cache_config_.app_prefix(), body_partition_size_); + ENVOY_LOG(debug, "New HazelcastClusterAccessor created."); + } + + try { + accessor_->connect(); + } catch (...) { + accessor_.reset(); + throw EnvoyException("Hazelcast Client could not connect to any cluster."); + } + + ENVOY_LOG(info, "HazelcastHttpCache has been started with profile: {}. Max body size: {}.", + unified_ ? "UNIFIED" + : "DIVIDED, partition size: " + std::to_string(body_partition_size_), + max_body_size_); + + HazelcastClusterAccessor& cluster_accessor = static_cast(*accessor_); + ENVOY_LOG(info, + "Cache statistics can be observed on Hazelcast Management Center" + " from the map named {}.", + unified_ ? cluster_accessor.responseMapName() : cluster_accessor.headerMapName()); +} + +void HazelcastHttpCache::shutdown(bool destroy) { + if (!accessor_) { + ENVOY_LOG(warn, "Cache is already offline."); + return; + } + if (accessor_->isRunning()) { + ENVOY_LOG(info, "Shutting down Hazelcast connection..."); + accessor_->disconnect(); + ENVOY_LOG(info, "Cache is offline now."); + } else { + ENVOY_LOG(warn, "Hazelcast client is already disconnected."); + } + if (destroy) { + accessor_.reset(); + } +} + +HazelcastHttpCache::~HazelcastHttpCache() { shutdown(true); } + +LookupContextPtr HazelcastHttpCache::makeLookupContext(LookupRequest&& request) { + if (unified_) { + return std::make_unique(*this, std::move(request)); + } else { + return std::make_unique(*this, std::move(request)); + } +} + +InsertContextPtr HazelcastHttpCache::makeInsertContext(LookupContextPtr&& lookup_context) { + ASSERT(lookup_context != nullptr); + if (unified_) { + return std::make_unique(*lookup_context, *this); + } else { + return std::make_unique(*lookup_context, *this); + } +} + +// TODO(enozcan): Implement when it's ready on the filter side. +// Depending on the filter's implementation, the cached entry's +// variant_key_ must be updated as well. Also, if vary headers +// change then the hash key of the response will change and +// updating only header map will not be enough in this case. +void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) { + ASSERT(lookup_context); + ASSERT(response_headers); + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; +} + +constexpr absl::string_view HazelcastCacheName = "envoy.extensions.http.cache.hazelcast"; + +// Cluster wide cache statistics should be observed on Hazelcast Management Center. +// They are not stored locally. +CacheInfo HazelcastHttpCache::cacheInfo() const { + CacheInfo cache_info; + cache_info.name_ = HazelcastCacheName; + cache_info.supports_range_requests_ = true; + return cache_info; +} + +std::string HazelcastHttpCacheFactory::name() const { return std::string(HazelcastCacheName); } + +ProtobufTypes::MessagePtr HazelcastHttpCacheFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +HttpCache& HazelcastHttpCacheFactory::getCache( + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { + if (!cache_) { + HazelcastHttpCacheConfig hz_cache_config; + MessageUtil::unpackTo(config.typed_config(), hz_cache_config); + cache_ = std::make_unique(hz_cache_config); + cache_->start(); + } + return *cache_; +} + +HttpCache& HazelcastHttpCacheFactory::getOfflineCache( + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { + if (!cache_) { + HazelcastHttpCacheConfig hz_cache_config; + MessageUtil::unpackTo(config.typed_config(), hz_cache_config); + cache_ = std::make_unique(hz_cache_config); + } + return *cache_; +} + +static Registry::RegisterFactory register_; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h similarity index 57% rename from source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h rename to source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index cd97afb39cc64..486df7c78f661 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -1,6 +1,11 @@ #pragma once -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" +#include "common/common/logger.h" +#include "common/runtime/runtime_impl.h" + +#include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h" #include "extensions/filters/http/cache/http_cache.h" namespace Envoy { @@ -9,8 +14,17 @@ namespace HttpFilters { namespace Cache { namespace HazelcastHttpCache { +// TODO(enozcan): Consider putting responses into cache with TTL derived from `max-age` header +// instead of using a common TTL for all. This is possible during insertion by passing TTL +// amount regardless of the configured TTL on Hazelcast server side. +// i.e: IMap::put(const K &key, const V &value, int64_t ttlInMilliseconds); + +using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; + /** - * Cache abstraction to support DIVIDED and UNIFIED cache modes. + * HttpCache implementation backed by Hazelcast. + * + * Supports two cache modes: UNIFIED and DIVIDED. * * In UNIFIED mode, an HTTP response is wrapped by a HazelcastResponseEntry * with its all fields (headers, body, trailers, request key) and stored in @@ -25,10 +39,10 @@ namespace HazelcastHttpCache { * number named to interrelate multiple entries belong to the same response. * */ -class HazelcastCache : public HttpCache { +class HazelcastHttpCache : public HttpCache, + public Logger::Loggable { public: - HazelcastCache(bool unified, uint64_t partition_size, uint64_t max_body_size) - : unified_(unified), body_partition_size_(partition_size), max_body_size_(max_body_size) {} + HazelcastHttpCache(HazelcastHttpCacheConfig config); /// Divided mode @@ -41,32 +55,37 @@ class HazelcastCache : public HttpCache { * same Hazelcast cluster might store the same response with different * keys. */ - virtual void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) PURE; + void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) { + accessor_->putHeader(mapKey(key), entry); + } /** * Puts a body entry into body cache. - * @note The key for a body partition must be obtainable from its header key. - * @param key Hash key for the whole body - * @param order Order of the body chunk among other partitions + * @param key Hash key for the whole body derived from the header + * @param order Order of the body chunk among other partitions starting from 0 * @param entry Entry to be inserted + * @note The key for a body partition must be obtainable from its header key. */ - virtual void putBody(const uint64_t key, const uint64_t order, - const HazelcastBodyEntry& entry) PURE; + void putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) { + accessor_->putBody(orderedMapKey(key, order), entry); + } /** * Performs a lookup to header cache for the given key. * @param key Hash key for the entry * @return HazelcastHeaderPtr to cached entry if found, nullptr otherwise */ - virtual HazelcastHeaderPtr getHeader(const uint64_t key) PURE; + HazelcastHeaderPtr getHeader(const uint64_t key) { return accessor_->getHeader(mapKey(key)); } /** * Performs a lookup to body cache for the given key and order pair. * @param key Hash key for the whole body * @param order Order of the body chunk among other partitions - * @return HazelcastBodyPtr to cached entry if found, nullptr otherwise. + * @return HazelcastBodyPtr to cached entry if found, nullptr otherwise */ - virtual HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) PURE; + HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) { + return accessor_->getBody(orderedMapKey(key, order)); + } /** * Cleans up a malformed response when at least one of the body chunks are missed @@ -77,7 +96,7 @@ class HazelcastCache : public HttpCache { * @param version Version for the key and body * @param body_size Total body size for the response */ - virtual void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) PURE; + void onMissingBody(uint64_t key, int32_t version, uint64_t body_size); /** * Cleans up a malformed response when a body partition with different version @@ -86,59 +105,64 @@ class HazelcastCache : public HttpCache { * @param version Version for the key and body * @param body_size Total body size for the response */ - virtual void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) PURE; + void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size); /// Unified mode /** * Puts a unified entry into unified cache if no other entry associated with the key * is found. + * @param key Hash key for the entry + * @param entry Entry to be inserted * @note IfAbsent is to prevent race between multiple filters. Overriding * an existing entry is forbidden. HttpCache::updateHeaders() should * be used if changing the header content is necessary. - * @param key Hash key for the entry - * @param entry Entry to be inserted */ - virtual void putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) PURE; + void putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) { + accessor_->putResponseIfAbsent(mapKey(key), entry); + } /** * Performs a lookup to unified cache for the given key. - * @param key Hash key for the entry. - * @return HazelcastResponsePtr to cached entry if found, nullptr otherwise. + * @param key Hash key for the entry + * @return HazelcastResponsePtr to cached entry if found, nullptr otherwise */ - virtual HazelcastResponsePtr getResponse(const uint64_t key) PURE; + HazelcastResponsePtr getResponse(const uint64_t key) { + return accessor_->getResponse(mapKey(key)); + } /// Common /** * Attempts to lock the given key in the cache. When a key is locked, a lookup - * can be performed but an insertion or update for the key must be prevented. + * can be performed but an insertion or update for the key must be prevented + * for threads other than the lock holder. + * @param key Key to be locked + * @return True if acquired, false otherwise * @note Used to prevent multiple insertions or updates by different * contexts at a time. - * @param key Key to be locked. - * @return True if acquired, false otherwise. */ - virtual bool tryLock(const uint64_t key) PURE; + bool tryLock(const uint64_t key) { return accessor_->tryLock(mapKey(key), unified_); } /** * Releases the lock for the key. * @param Key to be unlocked */ - virtual void unlock(const uint64_t key) PURE; + void unlock(const uint64_t key) { accessor_->unlock(mapKey(key), unified_); } /** * Produces a random number. - * @return Random unsigned long. - * @note The primary use case for the random number is to generate version - * for header and body entries. + * @return Random unsigned long + * @note The primary use case of the random number is to generate version + * for header and body entries in DIVIDED mode. */ - virtual uint64_t random() PURE; + uint64_t random() { return rand_.random(); } /** - * @note Ignored in UNIFIED mode. * @return Size in bytes for a single body entry configured for the cache + * @note Ignored in UNIFIED mode. */ - uint64_t bodySizePerEntry() { return body_partition_size_; }; + uint64_t bodySizePerEntry() { return body_partition_size_; } /** * @return Allowed max size in bytes for a response configured for the cache @@ -146,13 +170,13 @@ class HazelcastCache : public HttpCache { * than this limit, the first max_body_size_ bytes of the response * will be cached only. */ - uint64_t maxBodySize() { return max_body_size_; }; + uint64_t maxBodySize() { return max_body_size_; } /** * Generates a unique signed key for an unsigned one. * @param unsigned_key Unsigned hash key * @return Signed unique key - * @note Hazelcast client accepts signed keys only. + * @note Hazelcast client accepts signed map keys only. */ inline int64_t mapKey(const uint64_t unsigned_key) { // The reason for not static casting directly is a possible overflow @@ -168,7 +192,6 @@ class HazelcastCache : public HttpCache { * @param key Unsigned hash key for the header * @param order Order of the body among other partitions starting from 0 * @return Body partition key unique for header and order pair - * * @note Appending '#' or any other marker between the key and order * string is required. Otherwise, for instance, the 11th order * body for key 1 and the 1st order body for key 11 will have @@ -178,7 +201,30 @@ class HazelcastCache : public HttpCache { return std::to_string(key).append("#").append(std::to_string(order)); } - ~HazelcastCache() override = default; + /** + * Makes the cache ready to serve. Storage accessor connection must be established + * via StorageAccessor#connect() when the cache is started. + * + * @note Keeping this virtual allows tests to override access strategy. + * Using a local accessor will make the cache behavior testable without + * starting a Hazelcast instance. + */ + virtual void start(); + + /** + * Drops accessor connection to the storage. + * @param destroy True if accessor_ also should be destroyed. + */ + void shutdown(bool destroy); + + // from Cache::HttpCache + LookupContextPtr makeLookupContext(LookupRequest&& request) override; + InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; + void updateHeaders(LookupContextPtr&& lookup_context, + Http::ResponseHeaderMapPtr&& response_headers) override; + CacheInfo cacheInfo() const override; + + ~HazelcastHttpCache() override; protected: /** Cache mode */ @@ -187,8 +233,35 @@ class HazelcastCache : public HttpCache { /** Partition size in bytes for a single body entry */ const uint64_t body_partition_size_; - /** Allowed max size in bytes for a response */ + /** Allowed max body size in bytes for a response */ const uint64_t max_body_size_; + + /** Storage access */ + std::unique_ptr accessor_; + + /** typed config from CacheConfig */ + HazelcastHttpCacheConfig cache_config_; + + Runtime::RandomGeneratorImpl rand_; +}; + +class HazelcastHttpCacheFactory : public HttpCacheFactory { +public: + // UntypedFactory + std::string name() const override; + + // TypedFactory + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + // HttpCacheFactory + HttpCache& + getCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) override; + + HttpCache& // For testing only. + getOfflineCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config); + +private: + std::unique_ptr cache_; }; } // namespace HazelcastHttpCache diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc deleted file mode 100644 index 1e8e63c5120e1..0000000000000 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.cc +++ /dev/null @@ -1,267 +0,0 @@ -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" - -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" -#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" - -namespace Envoy { -namespace Extensions { -namespace HttpFilters { -namespace Cache { -namespace HazelcastHttpCache { - -LookupContextPtr HazelcastHttpCache::makeLookupContext(LookupRequest&& request) { - if (unified_) { - return std::make_unique(*this, std::move(request)); - } else { - return std::make_unique(*this, std::move(request)); - } -} - -InsertContextPtr HazelcastHttpCache::makeInsertContext(LookupContextPtr&& lookup_context) { - ASSERT(lookup_context != nullptr); - if (unified_) { - return std::make_unique(*lookup_context, *this); - } else { - return std::make_unique(*lookup_context, *this); - } -} - -void HazelcastHttpCache::updateHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) { - NOT_IMPLEMENTED_GCOVR_EXCL_LINE; - // TODO(enozcan): Enable when implemented on the filter side. - // Depending on the filter's implementation, the cached entry's - // variant_key_ must be updated as well. Also, if vary headers - // change then the hash key of the response will change and - // updating only header map will not be enough in this case. - ASSERT(lookup_context); - ASSERT(response_headers); - try { - if (unified_) { - updateUnifiedHeaders(std::move(lookup_context), std::move(response_headers)); - } else { - updateDividedHeaders(std::move(lookup_context), std::move(response_headers)); - } - } catch (HazelcastClientOfflineException& e) { - ENVOY_LOG(warn, "Hazelcast Connection is offline!"); - } catch (OperationTimeoutException& e) { - ENVOY_LOG(warn, "Updating headers has timed out."); - } catch (std::exception& e) { - ENVOY_LOG(warn, "Updating headers has failed: {}", e.what()); - } -} - -constexpr absl::string_view HazelcastCacheName = "envoy.extensions.http.cache.hazelcast"; - -// Cluster wide cache statistics should be observed on Hazelcast Management Center. -// They are not stored locally. -CacheInfo HazelcastHttpCache::cacheInfo() const { - CacheInfo cache_info; - cache_info.name_ = HazelcastCacheName; - cache_info.supports_range_requests_ = true; - return cache_info; -} - -void HazelcastHttpCache::putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) { - getHeaderMap().set(mapKey(key), entry); -} - -void HazelcastHttpCache::putBody(const uint64_t key, const uint64_t order, - const HazelcastBodyEntry& entry) { - getBodyMap().set(orderedMapKey(key, order), entry); -} - -HazelcastHeaderPtr HazelcastHttpCache::getHeader(const uint64_t key) { - return getHeaderMap().get(mapKey(key)); -} - -HazelcastBodyPtr HazelcastHttpCache::getBody(const uint64_t key, const uint64_t order) { - return getBodyMap().get(orderedMapKey(key, order)); -} - -void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { - try { - if (!tryLock(key)) { - // Let lock owner context to recover it. - return; - } - auto header = getHeader(key); - if (header && header->version() != version) { - // The missed body does not belong to the looked up header. Probably eviction and then - // insertion for the header has happened in the meantime. Since new insertion will - // override the existing bodies, ignore the cleanup and let orphan bodies (belong to - // evicted header, not overridden) be evicted by TTL as well. - unlock(key); - return; - } - int body_count = body_size / body_partition_size_; - while (body_count >= 0) { - getBodyMap().removeAsync(orderedMapKey(key, body_count--)); - } - getHeaderMap().remove(mapKey(key)); - unlock(key); - } catch (HazelcastClientOfflineException& e) { - // see DividedInsertContext#insertHeader() for left over locks on a connection failure. - ENVOY_LOG(warn, "Hazelcast Connection is offline!"); - } catch (OperationTimeoutException& e) { - ENVOY_LOG(warn, "Clean up for missing body has timed out."); - } catch (std::exception& e) { - ENVOY_LOG(warn, "Clean up for missing body has failed: {}", e.what()); - } -} - -void HazelcastHttpCache::onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) { - onMissingBody(key, version, body_size); -} - -void HazelcastHttpCache::putResponseIfAbsent(const uint64_t key, - const HazelcastResponseEntry& entry) { - getResponseMap().putIfAbsent(mapKey(key), entry); -} - -HazelcastResponsePtr HazelcastHttpCache::getResponse(const uint64_t key) { - return getResponseMap().get(mapKey(key)); -} - -bool HazelcastHttpCache::tryLock(const uint64_t key) { - // Internal lock mechanism of Hazelcast specific to map and key pair is - // used to make exactly one lookup context responsible for insertions and - // secure consistency during updateHeaders(). These locks prevent possible - // race for multiple cache filters from multiple proxies when they connect - // to the same Hazelcast cluster. - // The locks used here are re-entrant. A locked key can be acquired by - // the same thread again and again based on its pid. - return unified_ ? getResponseMap().tryLock(mapKey(key)) : getHeaderMap().tryLock(mapKey(key)); -} - -void HazelcastHttpCache::unlock(const uint64_t key) { - // Hazelcast does not allow a thread to unlock a key unless it's the key - // owner. To handle this, forceUnlock is called. - if (unified_) { - getResponseMap().forceUnlock(mapKey(key)); - } else { - getHeaderMap().forceUnlock(mapKey(key)); - } -} - -uint64_t HazelcastHttpCache::random() { return rand_.random(); } - -void HazelcastHttpCache::connect() { - if (hazelcast_client_ && hazelcast_client_->getLifecycleService().isRunning()) { - ENVOY_LOG(warn, "Client is already connected. Cluster name: {}", - hazelcast_client_->getClientConfig().getGroupConfig().getName()); - return; - } - - ClientConfig config = ConfigUtil::getClientConfig(cache_config_); - config.getSerializationConfig().addDataSerializableFactory( - HazelcastCacheEntrySerializableFactory::FACTORY_ID, - boost::shared_ptr( - new HazelcastCacheEntrySerializableFactory())); - - try { - hazelcast_client_ = std::make_unique(config); - } catch (...) { - throw EnvoyException("Hazelcast Client could not connect to any cluster."); - } - - ENVOY_LOG(info, "HazelcastHttpCache has been started with profile: {}. Max body size: {}.", - unified_ ? "UNIFIED" - : "DIVIDED, partition size: " + std::to_string(body_partition_size_), - max_body_size_); - ENVOY_LOG(info, - "Cache statistics can be observed on Hazelcast Management Center" - " from the map named {}.", - unified_ ? response_map_name_ : header_map_name_); -} - -void HazelcastHttpCache::shutdown(bool destroy) { - if (!hazelcast_client_) { - ENVOY_LOG(warn, "Client is already offline."); - return; - } - if (hazelcast_client_->getLifecycleService().isRunning()) { - ENVOY_LOG(info, "Shutting down Hazelcast connection..."); - hazelcast_client_->shutdown(); - ENVOY_LOG(info, "Cache is offline now."); - } else { - ENVOY_LOG(warn, "Cache is already offline."); - } - if (destroy) { - hazelcast_client_.reset(); - } -} - -HazelcastHttpCache::~HazelcastHttpCache() { shutdown(true); } - -void HazelcastHttpCache::updateUnifiedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) { - // TODO(enozcan): Implement when ready on the filter side. - ASSERT(!lookup_context); - ASSERT(!response_headers); -} - -void HazelcastHttpCache::updateDividedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) { - // TODO(enozcan): Implement when ready on the filter side. - ASSERT(!lookup_context); - ASSERT(!response_headers); -} - -std::string HazelcastHttpCache::constructMapName(const std::string& postfix) { - // Maps are differentiated by their names in Hazelcast cluster. Hence each - // plugin will connect to a map named with partition size and app_prefix. - // When a cache connects to a cluster which already has an active cache - // with different body_partition_size, this naming will prevent incompatibility - // and separate these two caches in the Hazelcast cluster. - std::string name(cache_config_.app_prefix()); - if (!unified_) { - name.append(":").append(std::to_string(body_partition_size_)); - } - return name.append("-").append(postfix); -} - -HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) - : HazelcastCache(config.unified(), ConfigUtil::validPartitionSize(config.body_partition_size()), - ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())), - cache_config_(config) { - body_map_name_ = constructMapName("body"); - header_map_name_ = constructMapName("div"); - response_map_name_ = constructMapName("uni"); -} - -std::string HazelcastHttpCacheFactory::name() const { return std::string(HazelcastCacheName); } - -ProtobufTypes::MessagePtr HazelcastHttpCacheFactory::createEmptyConfigProto() { - return std::make_unique(); -} - -HttpCache& HazelcastHttpCacheFactory::getCache( - const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { - if (!cache_) { - HazelcastHttpCacheConfig hz_cache_config; - MessageUtil::unpackTo(config.typed_config(), hz_cache_config); - cache_ = std::make_unique(hz_cache_config); - cache_->connect(); - } - return *cache_; -} - -HttpCache& HazelcastHttpCacheFactory::getOfflineCache( - const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { - if (!cache_) { - HazelcastHttpCacheConfig hz_cache_config; - MessageUtil::unpackTo(config.typed_config(), hz_cache_config); - cache_ = std::make_unique(hz_cache_config); - } - cache_->shutdown(false); - return *cache_; -} - -static Registry::RegisterFactory register_; - -} // namespace HazelcastHttpCache -} // namespace Cache -} // namespace HttpFilters -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h deleted file mode 100644 index e2af7100853d8..0000000000000 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h +++ /dev/null @@ -1,120 +0,0 @@ -#pragma once - -#include "envoy/registry/registry.h" - -#include "common/common/logger.h" -#include "common/runtime/runtime_impl.h" - -#include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" - -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache.h" - -#include "hazelcast/client/HazelcastClient.h" -#include "hazelcast/client/IMap.h" - -namespace Envoy { -namespace Extensions { -namespace HttpFilters { -namespace Cache { -namespace HazelcastHttpCache { - -using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; -using hazelcast::client::IMap; - -// TODO(enozcan): Consider putting responses into cache with TTL derived from `max-age` header -// instead of using a common TTL for all. This is possible during insertion by passing TTL -// amount regardless of the configured TTL on Hazelcast server side. -// i.e: IMap::put(const K &key, const V &value, int64_t ttlInMilliseconds); - -class HazelcastHttpCache : public HazelcastCache, - public Logger::Loggable { -public: - HazelcastHttpCache(HazelcastHttpCacheConfig config); - - // from Cache::HttpCache - LookupContextPtr makeLookupContext(LookupRequest&& request) override; - InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; - void updateHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) override; - CacheInfo cacheInfo() const override; - - // from HazelcastCache - void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) override; - void putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) override; - HazelcastHeaderPtr getHeader(const uint64_t key) override; - HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) override; - void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) override; - void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) override; - void putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) override; - HazelcastResponsePtr getResponse(const uint64_t key) override; - bool tryLock(const uint64_t key) override; - void unlock(const uint64_t key) override; - uint64_t random() override; - - void connect(); - void shutdown(bool destroy); - - ~HazelcastHttpCache() override; - -private: - friend class HazelcastHttpCacheTestBase; - friend class HazelcastTestableRemoteCache; - - void updateUnifiedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers); - - void updateDividedHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers); - - /** Generates a unique map name to this cache with the given postfix. */ - std::string constructMapName(const std::string& postfix); - - /** Returns remote header cache proxy */ - inline IMap getHeaderMap() { - return hazelcast_client_->getMap(header_map_name_); - } - - /** Returns remote body cache proxy */ - inline IMap getBodyMap() { - return hazelcast_client_->getMap(body_map_name_); - } - - /** Returns remote response cache proxy */ - inline IMap getResponseMap() { - return hazelcast_client_->getMap(response_map_name_); - } - - std::unique_ptr hazelcast_client_; - HazelcastHttpCacheConfig cache_config_; - - std::string body_map_name_; - std::string header_map_name_; - std::string response_map_name_; - - Runtime::RandomGeneratorImpl rand_; -}; - -class HazelcastHttpCacheFactory : public HttpCacheFactory { -public: - // UntypedFactory - std::string name() const override; - - // TypedFactory - ProtobufTypes::MessagePtr createEmptyConfigProto() override; - - // HttpCacheFactory - HttpCache& - getCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) override; - - HttpCache& // For testing only. - getOfflineCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config); - -private: - std::unique_ptr cache_; -}; - -} // namespace HazelcastHttpCache -} // namespace Cache -} // namespace HttpFilters -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc new file mode 100644 index 0000000000000..92a6bc2ab4902 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc @@ -0,0 +1,108 @@ +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +void HazelcastClusterAccessor::putHeader(const int64_t key, const HazelcastHeaderEntry& value) { + getHeaderMap().set(key, value); +} + +void HazelcastClusterAccessor::putBody(const std::string& key, const HazelcastBodyEntry& value) { + getBodyMap().set(key, value); +} + +HazelcastHeaderPtr HazelcastClusterAccessor::getHeader(const int64_t key) { + return getHeaderMap().get(key); +} + +HazelcastBodyPtr HazelcastClusterAccessor::getBody(const std::string& key) { + return getBodyMap().get(key); +} + +void HazelcastClusterAccessor::removeBodyAsync(const std::string& key) { + getBodyMap().removeAsync(key); +} + +void HazelcastClusterAccessor::removeHeader(const int64_t key) { getHeaderMap().deleteEntry(key); } + +void HazelcastClusterAccessor::putResponseIfAbsent(const int64_t key, + const HazelcastResponseEntry& value) { + getResponseMap().putIfAbsent(key, value); +} + +HazelcastResponsePtr HazelcastClusterAccessor::getResponse(const int64_t key) { + return getResponseMap().get(key); +} + +// Internal lock mechanism of Hazelcast specific to map and key pair is +// used to make exactly one lookup context responsible for insertions. It +// is and also used to secure consistency during updateHeaders(). These +// locks prevent possible race for multiple cache filters from multiple +// proxies when they connect to the same Hazelcast cluster. The locks used +// here are re-entrant. A locked key can be acquired by the same thread +// again and again based on its pid. +bool HazelcastClusterAccessor::tryLock(const int64_t key, bool unified) { + return unified ? getResponseMap().tryLock(key) : getHeaderMap().tryLock(key); +} + +// Hazelcast does not allow a thread to unlock a key unless it's the key +// owner. To handle this, IMap::forceUnlock is called here to make sure +// the lock is released certainly. +void HazelcastClusterAccessor::unlock(const int64_t key, bool unified) { + if (unified) { + getResponseMap().forceUnlock(key); + } else { + getHeaderMap().forceUnlock(key); + } +} + +bool HazelcastClusterAccessor::isRunning() { + if (hazelcast_client_) { + return hazelcast_client_->getLifecycleService().isRunning(); + } + return false; +} + +std::string HazelcastClusterAccessor::clusterName() { + return hazelcast_client_->getClientConfig().getGroupConfig().getName(); +} + +void HazelcastClusterAccessor::disconnect() { hazelcast_client_->shutdown(); } + +const std::string& HazelcastClusterAccessor::headerMapName() { return header_map_name_; } + +const std::string& HazelcastClusterAccessor::responseMapName() { return response_map_name_; } + +HazelcastClusterAccessor::HazelcastClusterAccessor(ClientConfig&& client_config, + const std::string& app_prefix, + const uint64_t partition_size) + : app_prefix_(app_prefix), partition_size_(partition_size), + client_config_(std::move(client_config)) { + body_map_name_ = constructMapName("body", false); + header_map_name_ = constructMapName("div", false); + response_map_name_ = constructMapName("uni", true); +} + +void HazelcastClusterAccessor::connect() { + if (hazelcast_client_ && hazelcast_client_->getLifecycleService().isRunning()) { + return; + } + hazelcast_client_ = std::make_unique(client_config_); +} + +std::string HazelcastClusterAccessor::constructMapName(const std::string& postfix, bool unified) { + std::string name(app_prefix_); + if (!unified) { + name.append(":").append(std::to_string(partition_size_)); + } + return name.append("-").append(postfix); +} + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h new file mode 100644 index 0000000000000..473b19edd8488 --- /dev/null +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h @@ -0,0 +1,128 @@ +#pragma once + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" + +#include "hazelcast/client/HazelcastClient.h" +#include "hazelcast/client/IMap.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +/** + * Abstraction for storage connections of the cache. + * + * @note Decoupled from the cache in favor of local storage implementations + * or mocks to test the cache without running a real Hazelcast Instance. + */ +class StorageAccessor { +public: + StorageAccessor() {} + + virtual void putHeader(const int64_t key, const HazelcastHeaderEntry& value) PURE; + virtual void putBody(const std::string& key, const HazelcastBodyEntry& value) PURE; + virtual void putResponseIfAbsent(const int64_t key, const HazelcastResponseEntry& value) PURE; + + virtual HazelcastHeaderPtr getHeader(const int64_t key) PURE; + virtual HazelcastBodyPtr getBody(const std::string& key) PURE; + virtual HazelcastResponsePtr getResponse(const int64_t key) PURE; + + virtual void removeBodyAsync(const std::string& key) PURE; + virtual void removeHeader(const int64_t key) PURE; + + virtual bool tryLock(const int64_t key, bool unified) PURE; + virtual void unlock(const int64_t key, bool unified) PURE; + + virtual bool isRunning() PURE; + virtual std::string clusterName() PURE; + + virtual void connect() PURE; + virtual void disconnect() PURE; + + virtual ~StorageAccessor() = default; +}; + +/** + * Accessor to Hazelcast Cluster. + * + * The cache uses this accessor in the production code. + */ +class HazelcastClusterAccessor : public StorageAccessor { +public: + HazelcastClusterAccessor(ClientConfig&& client_config, const std::string& app_prefix, + const uint64_t partition_size); + + void putHeader(const int64_t key, const HazelcastHeaderEntry& value) override; + void putBody(const std::string& key, const HazelcastBodyEntry& value) override; + void putResponseIfAbsent(const int64_t key, const HazelcastResponseEntry& value) override; + + HazelcastHeaderPtr getHeader(const int64_t key) override; + HazelcastBodyPtr getBody(const std::string& key) override; + HazelcastResponsePtr getResponse(const int64_t key) override; + + void removeBodyAsync(const std::string& key) override; + void removeHeader(const int64_t key) override; + + bool tryLock(const int64_t key, bool unified) override; + void unlock(const int64_t key, bool unified) override; + + bool isRunning() override; + std::string clusterName() override; + + void connect() override; + void disconnect() override; + + const std::string& headerMapName(); + const std::string& responseMapName(); + + virtual ~HazelcastClusterAccessor() = default; + +private: + friend class RemoteTestAccessor; + + /** + * Generates a map name unique to the cache configuration. + * + * @note Maps with the same key & value types are differentiated by their names in + * Hazelcast cluster. Hence each plugin will connect to a map named with partition + * size and app_prefix. When a cache connects to a cluster which already has an active + * cache with different body_partition_size, this naming will prevent incompatibility + * and separate these two caches in the Hazelcast cluster. + */ + std::string constructMapName(const std::string& postfix, bool unified); + + /** Returns remote header cache proxy */ + inline IMap getHeaderMap() { + return hazelcast_client_->getMap(header_map_name_); + } + + /** Returns remote body cache proxy */ + inline IMap getBodyMap() { + return hazelcast_client_->getMap(body_map_name_); + } + + /** Returns remote response cache proxy */ + inline IMap getResponseMap() { + return hazelcast_client_->getMap(response_map_name_); + } + + std::unique_ptr hazelcast_client_; + + // From HazelcastCacheConfig + const std::string& app_prefix_; + uint64_t partition_size_; + + ClientConfig client_config_; + + std::string body_map_name_; + std::string header_map_name_; + std::string response_map_name_; +}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h index 4450312e2d2f6..e927154095ce8 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -64,6 +64,8 @@ class ConfigUtil { static uint16_t partitionWarnLimit() { return PARTITION_WARN_LIMIT; } private: + friend class ConfigUtilsTest; + // After this much body partitions stored for a response in DIVIDED mode, // a suggestion log will be appeared to increase partition size. static constexpr uint16_t PARTITION_WARN_LIMIT = 16; @@ -94,8 +96,6 @@ class ConfigUtil { // Duration for an invocation to be cancelled. static constexpr uint32_t DEFAULT_INVOCATION_TIMEOUT_SEC = 8; - - friend class ConfigUtilsTest; }; } // namespace HazelcastHttpCache diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 6c4c305276b81..4798a1f828a3f 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -10,8 +10,8 @@ load( envoy_package() envoy_extension_cc_test( - name = "hazelcast_unified_cache_test", - srcs = ["hazelcast_unified_cache_test.cc"], + name = "hazelcast_cache_config_test", + srcs = ["hazelcast_cache_config_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", @@ -19,8 +19,8 @@ envoy_extension_cc_test( ) envoy_extension_cc_test( - name = "hazelcast_divided_cache_test", - srcs = ["hazelcast_divided_cache_test.cc"], + name = "hazelcast_common_cache_test", + srcs = ["hazelcast_common_cache_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", @@ -28,8 +28,8 @@ envoy_extension_cc_test( ) envoy_extension_cc_test( - name = "hazelcast_common_cache_test", - srcs = ["hazelcast_common_cache_test.cc"], + name = "hazelcast_divided_cache_test", + srcs = ["hazelcast_divided_cache_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", @@ -37,8 +37,8 @@ envoy_extension_cc_test( ) envoy_extension_cc_test( - name = "hazelcast_config_utils_test", - srcs = ["config_utils_test.cc"], + name = "hazelcast_entry_serialization_test", + srcs = ["hazelcast_entry_serialization_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", @@ -46,8 +46,8 @@ envoy_extension_cc_test( ) envoy_extension_cc_test( - name = "hazelcast_serialization_test", - srcs = ["serialization_test.cc"], + name = "hazelcast_unified_cache_test", + srcs = ["hazelcast_unified_cache_test.cc"], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ ":hazelcast_test_lib", @@ -56,10 +56,10 @@ envoy_extension_cc_test( envoy_extension_cc_test_library( name = "hazelcast_test_lib", - srcs = ["hazelcast_test_cache.cc"], hdrs = [ - "hazelcast_test_cache.h", - "util.h", + "test_accessors.h", + "test_caches.h", + "test_util.h", ], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc similarity index 98% rename from test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc rename to test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc index d448ff1831121..80206a67a3624 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/config_utils_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc @@ -12,6 +12,7 @@ using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; class ConfigUtilsTest : public testing::Test { protected: + // Private configuration values from HazelcastHttpCache::ConfigUtil uint64_t defaultPartitionSize() { return ConfigUtil::DEFAULT_PARTITION_SIZE; } uint64_t maxPartitionSize() { return ConfigUtil::MAX_ALLOWED_PARTITION_SIZE; } uint64_t maxUnifiedBodySize() { return ConfigUtil::MAX_ALLOWED_UNIFIED_BODY_SIZE; } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 8c0265f6971c2..33014e0ffdbf5 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -2,7 +2,7 @@ #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" -#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h" namespace Envoy { namespace Extensions { @@ -18,12 +18,19 @@ class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, protected: void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(GetParam()); - // To test the cache with a real Hazelcast instance, remote cache - // must be used during tests. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); - cache_->restoreTestConnection(); - cache_->clearTestMaps(); + // To test the cache with a real Hazelcast instance, use remote test cache. + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); + cache_->start(); + cache_->getTestAccessor().clearMaps(); + } + + Key getVariantKey(LookupContextPtr& lookup, + std::vector>& headers) { + HazelcastLookupContextBase& hz_lookup = static_cast(*lookup); + Key variant_key = hz_lookup.variantKey(); + hz_lookup.arrangeVariantHeaders(variant_key, headers); + return variant_key; } }; @@ -32,7 +39,7 @@ INSTANTIATE_TEST_SUITE_P(CommonCacheTests, HazelcastHttpCacheTest, ::testing::Bo TEST_P(HazelcastHttpCacheTest, MissPutAndGetEntries) { // To test divided body behavior as well, bodies having sizes near the limit are preferred. const std::string RequestPath1("/body/with/limit/size/plus/one"); - const std::string RequestPath2("/body/with/exactly/limit/size"); + const std::string RequestPath2("/body/with/exact/limit/size"); const std::string RequestPath3("/body/with/limit/size/minus/one"); LookupContextPtr lookup_context1 = lookup(RequestPath1); @@ -58,40 +65,53 @@ TEST_P(HazelcastHttpCacheTest, MissPutAndGetEntries) { TEST_P(HazelcastHttpCacheTest, HandleRangedResponses) { const std::string RequestPath("/ranged/responses"); - - LookupContextPtr lookup_context1 = lookup(RequestPath); + LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); int size = HazelcastTestUtil::TEST_PARTITION_SIZE; - const std::string Body1 = std::string(size, 'h'); - const std::string Body2 = std::string(size, 'z'); - const std::string Body3 = std::string(size, 'c'); - const std::string Body = Body1 + Body2 + Body3; - - insert(move(lookup_context1), getResponseHeaders(), Body); - lookup_context1 = lookup(RequestPath); + const std::string Body = std::string(size, 'h') + std::string(size, 'z') + std::string(size, 'c'); + insert(move(lookup_context), getResponseHeaders(), Body); + lookup_context = lookup(RequestPath); // 'h' * (size) - EXPECT_EQ(absl::string_view(Body.c_str(), size), getBody(*lookup_context1, 0, size)); + EXPECT_EQ(absl::string_view(Body.c_str(), size), getBody(*lookup_context, 0, size)); // 'z' * (size) - EXPECT_EQ(absl::string_view(Body.c_str() + size, size), - getBody(*lookup_context1, size, size * 2)); + EXPECT_EQ(absl::string_view(Body.c_str() + size, size), getBody(*lookup_context, size, size * 2)); // 'h' * (size/2) + 'z' * (size/2) EXPECT_EQ(absl::string_view(Body.c_str() + size / 2, size), - getBody(*lookup_context1, size / 2, size + size / 2)); + getBody(*lookup_context, size / 2, size + size / 2)); // 'h' + 'z' * (size) + 'c' EXPECT_EQ(absl::string_view(Body.c_str() + size - 1, size + 2), - getBody(*lookup_context1, size - 1, 2 * size + 1)); + getBody(*lookup_context, size - 1, 2 * size + 1)); // 'h' * (size) + 'z' * (size) + 'c' * (size) - EXPECT_EQ(absl::string_view(Body.c_str(), size * 3), getBody(*lookup_context1, 0, size * 3)); + EXPECT_EQ(absl::string_view(Body.c_str(), size * 3), getBody(*lookup_context, 0, size * 3)); +} + +TEST_P(HazelcastHttpCacheTest, VariantKeyTest) { + // The same key should be created for the same vary headers even if their + // order is different. + LookupContextPtr lookup_context = lookup("/variant/key/test/"); + std::vector> vary_headers; + vary_headers.push_back(std::make_pair("Accept-Language", "tr;q=0.8")); + vary_headers.push_back(std::make_pair("User-Agent", "desktop")); + auto key1 = getVariantKey(lookup_context, vary_headers); + vary_headers.clear(); + vary_headers.push_back(std::make_pair("User-Agent", "desktop")); + vary_headers.push_back(std::make_pair("Accept-Language", "tr;q=0.8")); + auto key2 = getVariantKey(lookup_context, vary_headers); + + EXPECT_EQ(4, key1.custom_fields_size()); // 2 keys, 2 values, 4 in total + EXPECT_EQ(4, key2.custom_fields_size()); + EXPECT_TRUE(Envoy::Protobuf::util::MessageDifferencer::Equals(key1, key2)); + EXPECT_EQ(stableHashKey(key1), stableHashKey(key2)); } // -// Tests belong to SimpleHttpCache are applied below with minor changes on the test body. +// Tests belong to SimpleHttpCache are applied below with minor changes on the test bodies. // TEST_P(HazelcastHttpCacheTest, SimplePutGet) { const std::string RequestPath1("/simple/put/first"); @@ -130,7 +150,8 @@ TEST_P(HazelcastHttpCacheTest, Miss) { EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // Do not left over a missed lookup without inserting or releasing its lock. - cache_->base().unlock(variant_hash_key); + // This is required for RemoteTestCache. + cache_->unlock(variant_hash_key); } TEST_P(HazelcastHttpCacheTest, Fresh) { @@ -178,7 +199,7 @@ TEST_P(HazelcastHttpCacheTest, ResponseStaleWithRequestLargeMaxStale) { } TEST_P(HazelcastHttpCacheTest, StreamingPutAndRangeGet) { - InsertContextPtr inserter = cache_->base().makeInsertContext(lookup("/streaming/put")); + InsertContextPtr inserter = cache_->makeInsertContext(lookup("/streaming/put")); inserter->insertHeaders(getResponseHeaders(), false); inserter->insertBody( Buffer::OwnedImpl("Hello, "), [](bool ready) { EXPECT_TRUE(ready); }, false); @@ -203,10 +224,11 @@ TEST(Registration, GetFactory) { config.mutable_typed_config()->PackFrom(hz_cache_config); // getOfflineCache() call is for testing. It creates a HazelcastHttpCache but does - // not make it operational until a connect() call. + // not make it operational until a start() call. HttpCache& cache = static_cast(factory)->getOfflineCache(config); EXPECT_EQ(cache.cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); - EXPECT_THROW_WITH_MESSAGE(static_cast(cache).connect(), EnvoyException, + HazelcastHttpCache& hz_cache = static_cast(cache); + EXPECT_THROW_WITH_MESSAGE(hz_cache.start(), EnvoyException, "Hazelcast Client could not connect to any cluster."); } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 96048c2c1ea5c..2aef744c8df14 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -1,6 +1,6 @@ #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" -#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h" namespace Envoy { namespace Extensions { @@ -15,18 +15,17 @@ class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { protected: void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(false); - // To test the cache with a real Hazelcast instance, remote cache - // must be used during tests. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); - cache_->restoreTestConnection(); - cache_->clearTestMaps(); + // To test the cache with a real Hazelcast instance, use remote test cache. + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); + cache_->start(); + cache_->getTestAccessor().clearMaps(); } }; TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { const std::string RequestPath("/abort/when/max/size/reached"); - InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup(RequestPath)); + InsertContextPtr insert_context = cache_->makeInsertContext(lookup(RequestPath)); insert_context->insertHeaders(getResponseHeaders(), false); bool ready_for_next = true; while (ready_for_next) { @@ -37,7 +36,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { EXPECT_EQ(((HazelcastTestUtil::TEST_MAX_BODY_SIZE + HazelcastTestUtil::TEST_PARTITION_SIZE - 1) / HazelcastTestUtil::TEST_PARTITION_SIZE), - cache_->bodyTestMapSize()); + cache_->getTestAccessor().bodyMapSize()); EXPECT_TRUE(expectLookupSuccessWithFullBody( lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_MAX_BODY_SIZE, 'h'))); } @@ -52,13 +51,13 @@ TEST_F(HazelcastDividedCacheTest, PreventOverridingCacheEntries) { lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); - // A possible call to insertion below (using a success lookup context) would be the filter's - // fault, not an expected behavior. + // A possible call to insertion - made on purpose below, would be the filter's + // fault, not an expected behavior. Cache prevents the insertion in such a case. const std::string OverriddenBody(HazelcastTestUtil::TEST_PARTITION_SIZE * 3, 'z'); insert(move(lookup_context), getResponseHeaders(), OverriddenBody); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath).get(), OriginalBody)); - EXPECT_EQ(2, cache_->bodyTestMapSize()); - EXPECT_EQ(1, cache_->headerTestMapSize()); + EXPECT_EQ(2, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(1, cache_->getTestAccessor().headerMapSize()); } TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { @@ -107,10 +106,10 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath1).get(), Body)); // Change version of the second partition. - auto body2 = cache_->base().getBody(variant_hash_key, 1); + auto body2 = cache_->getBody(variant_hash_key, 1); EXPECT_NE(body2, nullptr); body2->version(body2->version() + 1); - cache_->base().putBody(variant_hash_key, 1, *body2); + cache_->putBody(variant_hash_key, 1, *body2); // Change happened in the second partition. Lookup to the first one should be successful. lookup_context = lookup(RequestPath1); @@ -121,8 +120,8 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { EXPECT_EQ(fullBody, HazelcastTestUtil::abortedBodyResponse()); // Clean up must be performed for malformed entries. - EXPECT_EQ(0, cache_->bodyTestMapSize()); - EXPECT_EQ(0, cache_->headerTestMapSize()); + EXPECT_EQ(0, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(0, cache_->getTestAccessor().headerMapSize()); } TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { @@ -141,12 +140,12 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. - auto header = cache_->base().getHeader(variant_hash_key); + auto header = cache_->getHeader(variant_hash_key); Key modified = header->variantKey(); modified.add_custom_fields("custom1"); modified.add_custom_fields("custom2"); header->variantKey(std::move(modified)); - cache_->base().putHeader(variant_hash_key, *header); + cache_->putHeader(variant_hash_key, *header); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -157,10 +156,10 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - EXPECT_EQ(1, cache_->headerTestMapSize()); + EXPECT_EQ(1, cache_->getTestAccessor().headerMapSize()); - auto modified_header = cache_->base().getHeader(variant_hash_key); - EXPECT_EQ(2, modified_header->variantKey().custom_fields_size()); + auto modified_header = cache_->getHeader(variant_hash_key); + EXPECT_EQ(*header, *modified_header); } TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { @@ -184,7 +183,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { // variant_hash_key "2" -> Body3 (in body map) EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); - cache_->removeTestBody(variant_hash_key, 1); // evict Body2. + cache_->getTestAccessor().removeBody(cache_->orderedMapKey(variant_hash_key, 1)); // evict Body2. lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); @@ -193,24 +192,63 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { lookup_context1->getBody({0, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, [](Buffer::InstancePtr&& data) { EXPECT_NE(data, nullptr); }); - // Lookup for Body2 must fail and trigger clean up. - lookup_context1->getBody( - {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, - [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); + { + std::thread t1([&] { + // If another thread locks the key, then the current one should not perform + // clean up. The lock here will serve the purpose. + EXPECT_TRUE(cache_->tryLock(variant_hash_key)); + }); + t1.join(); + + // Lookup for Body2 must fail and trigger clean up. But due to locked key (might + // be acquired by another clean up process for this entry), clean up must do no-op. + lookup_context1->getBody( + {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, + [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); + + EXPECT_NE(0, cache_->getTestAccessor().bodyMapSize()); // clean up is not performed. + + cache_->unlock(variant_hash_key); + } - lookup_context1 = lookup(RequestPath1); - EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + { + // Clean up must be aborted when header versions are mismatched. + // This prevents clean up operation for wrong entries. + auto header = cache_->getHeader(variant_hash_key); + int32_t original_version = header->version(); + header->version(original_version - 1); + cache_->putHeader(variant_hash_key, *header); - // On lookup miss, lock is being acquired. It must be released - // explicitly or let context do the insertion and then release. - // If not released, the second run for the test fails. Since no - // insertion follows the missed lookup here, the lock is explicitly - // released. - cache_->base().unlock(variant_hash_key); + lookup_context1->getBody( + {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, + [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); - // Assert clean up - EXPECT_EQ(0, cache_->bodyTestMapSize()); - EXPECT_EQ(0, cache_->headerTestMapSize()); + EXPECT_NE(0, cache_->getTestAccessor().bodyMapSize()); + + header->version(original_version); + cache_->putHeader(variant_hash_key, *header); + } + + { + // Clean up must be performed after body miss. + lookup_context1->getBody( + {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, + [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); + + lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // On lookup miss, lock is being acquired. It must be released + // explicitly or let context do the insertion and then release. + // If not released, the second run for the test fails. Since no + // insertion follows the missed lookup here, the lock is explicitly + // released. + cache_->unlock(variant_hash_key); + + // Assert clean up + EXPECT_EQ(0, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(0, cache_->getTestAccessor().headerMapSize()); + } } TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { @@ -230,11 +268,13 @@ TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { // then empty body for body insertion. headerOnlyTest("/empty/body/response", true); - EXPECT_EQ(0, cache_->bodyTestMapSize()); - EXPECT_EQ(2, cache_->headerTestMapSize()); + EXPECT_EQ(0, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(2, cache_->getTestAccessor().headerMapSize()); } TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { + // Operations are arranged to test all exception using Local Test Accessor. + // Changing the order might not cause test to fail but to uncover some exceptions. { const std::string RequestPath("/connection/lost/after/insertion"); LookupContextPtr lookup_context = lookup(RequestPath); @@ -245,13 +285,23 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { lookup_context = lookup(RequestPath); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); - cache_->dropTestConnection(); + cache_->getTestAccessor().dropConnection(); + + // std::exception case. + lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // HazelcastClientOfflineException case. + lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + // OperationTimeoutException case. lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + insert(move(lookup_context), getResponseHeaders(), Body); - cache_->restoreTestConnection(); + cache_->getTestAccessor().restoreConnection(); lookup_context = lookup(RequestPath); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); @@ -259,7 +309,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { { const std::string RequestPath("/connection/lost/during/insertion"); - InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup(RequestPath)); + InsertContextPtr insert_context = cache_->makeInsertContext(lookup(RequestPath)); insert_context->insertHeaders(getResponseHeaders(), false); auto insert = [&insert_context](std::string body, bool end_stream) { insert_context->insertBody( @@ -269,21 +319,127 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'), false); insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z'), false); - cache_->dropTestConnection(); + cache_->getTestAccessor().dropConnection(); - insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'), false); + // testing std::exception case. + insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'), true); + // testing HazelcastClientOfflineException case. insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 's'), true); + // testing OperationTimeoutException case. + insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 't'), true); LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - cache_->restoreTestConnection(); + cache_->getTestAccessor().restoreConnection(); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); } } +TEST_F(HazelcastDividedCacheTest, FailDuringLock) { + // Tests the case when header lookup is performed without exception but tryLock for + // the insertion permission is failed. Operations are arranged to test all exception + // using Local Test Accessor. Changing the order might not cause test to fail but + // to uncover some exceptions. + + const std::string RequestPath("/failed/during/try/lock"); + + // This will cause LocalTestAccessor::tryLock to raise error. + cache_->getTestAccessor().failOnLock(); + + std::thread t1([&] { + // To make RemoteTestCache failOnLock as LocalTestCache does, HazelcastHttpCache + // must be modified. In order to prevent this modification, making a lookup here + // to make the key locked. Hence the lookups below will throw exception for local + // cache and return false for remote cache. This has no effect on local test but + // lack of this lookup causes remote test to fail. + lookup(RequestPath); + }); + t1.join(); + + lookup(RequestPath); // std::exception + lookup(RequestPath); // HazelcastClientOfflineException + LookupContextPtr lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + insert(move(lookup_context), getResponseHeaders(), ""); // must be aborted. + lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); +} + +TEST_F(HazelcastDividedCacheTest, FailDuringBodyLookupWhenHeaderSucceeds) { + // Tests the case when header lookup succeeds but body lookup fails. + const int body_size = HazelcastTestUtil::TEST_PARTITION_SIZE * 2; + const std::string RequestPath("/fail/on/body"); + const std::string Body('h', body_size); + + LookupContextPtr lookup_context = lookup(RequestPath); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + insert(move(lookup_context), getResponseHeaders(), Body); + lookup_context = lookup(RequestPath); + // lookup for header is OK. + EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); + + // Lookup for Body1 is OK. + lookup_context->getBody({0, HazelcastTestUtil::TEST_PARTITION_SIZE}, + [](Buffer::InstancePtr&& data) { EXPECT_NE(data, nullptr); }); + + cache_->getTestAccessor().dropConnection(); + + // Lookup for body should be aborted on HazelcastOffline exception. + lookup_context->getBody({0, body_size}, + [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); + + // Lookup for body should be aborted on TimeOut exception. + lookup_context->getBody({0, body_size}, + [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); + + // Lookup for body should be aborted on std::exception. + lookup_context->getBody({0, body_size}, + [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); + + cache_->getTestAccessor().restoreConnection(); + + EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); +} + +TEST_F(HazelcastDividedCacheTest, AbortInsertionWhenLockLeftover) { + // Happens when a lookup context acquires the lock for insertion but fails to + // unlock before insertion. In such a case, the fail over must be done by + // max.leaseTime of locks set on the Hazelcast server. + const std::string RequestPath1("/connection/lost/on/lock/acquisition/1"); + const std::string RequestPath2("/connection/lost/on/lock/acquisition/2"); + LookupContextPtr lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + LookupContextPtr lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + InsertContextPtr insert_context1 = cache_->makeInsertContext(std::move(lookup_context1)); + InsertContextPtr insert_context2 = cache_->makeInsertContext(std::move(lookup_context2)); + + cache_->getTestAccessor().dropConnection(); + insert_context1->insertHeaders(getResponseHeaders(), true); + insert_context2->insertHeaders(getResponseHeaders(), true); + cache_->getTestAccessor().restoreConnection(); + + lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + insert(move(lookup_context1), getResponseHeaders(), ""); + // insertion fails since the key is still locked. + lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + insert(move(lookup_context2), getResponseHeaders(), ""); + // insertion fails since the key is still locked. + lookup_context2 = lookup(RequestPath2); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); +} + } // namespace HazelcastHttpCache } // namespace Cache } // namespace HttpFilters diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/serialization_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_entry_serialization_test.cc similarity index 100% rename from test/extensions/filters/http/cache/hazelcast_http_cache/serialization_test.cc rename to test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_entry_serialization_test.cc diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc deleted file mode 100644 index da6473067ef6c..0000000000000 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.cc +++ /dev/null @@ -1,188 +0,0 @@ -#include "test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h" - -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" - -namespace Envoy { -namespace Extensions { -namespace HttpFilters { -namespace Cache { -namespace HazelcastHttpCache { - -HazelcastTestableLocalCache::HazelcastTestableLocalCache(HazelcastHttpCacheConfig config) - : HazelcastTestableHttpCache(dynamic_cast(*this)), - HazelcastCache(config.unified(), config.body_partition_size(), config.max_body_size()) {} - -void HazelcastTestableLocalCache::clearTestMaps() { - header_map_.clear(); - body_map_.clear(); - response_map_.clear(); -} - -void HazelcastTestableLocalCache::dropTestConnection() { connected_ = false; } - -void HazelcastTestableLocalCache::restoreTestConnection() { connected_ = true; } - -int HazelcastTestableLocalCache::headerTestMapSize() { - ASSERT(!unified_); - return header_map_.size(); -} - -int HazelcastTestableLocalCache::bodyTestMapSize() { - ASSERT(!unified_); - return body_map_.size(); -} - -int HazelcastTestableLocalCache::responseTestMapSize() { - ASSERT(unified_); - return response_map_.size(); -} - -void HazelcastTestableLocalCache::putTestResponse(uint64_t key, - const HazelcastResponseEntry& entry) { - checkConnection(); - response_map_[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); -} - -void HazelcastTestableLocalCache::removeTestBody(uint64_t key, uint64_t order) { - checkConnection(); - body_map_.erase(orderedMapKey(key, order)); -} - -LookupContextPtr HazelcastTestableLocalCache::makeLookupContext(LookupRequest&& request) { - if (unified_) { - return std::make_unique(*this, std::move(request)); - } else { - return std::make_unique(*this, std::move(request)); - } -} - -InsertContextPtr HazelcastTestableLocalCache::makeInsertContext(LookupContextPtr&& lookup_context) { - ASSERT(lookup_context != nullptr); - if (unified_) { - return std::make_unique(*lookup_context, *this); - } else { - return std::make_unique(*lookup_context, *this); - } -} - -void HazelcastTestableLocalCache::updateHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) { - // Not implemented and tested yet. - ASSERT(lookup_context); - ASSERT(response_headers); -} - -CacheInfo HazelcastTestableLocalCache::cacheInfo() const { return {}; } - -void HazelcastTestableLocalCache::putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) { - checkConnection(); - header_map_[mapKey(key)] = HazelcastHeaderPtr(new HazelcastHeaderEntry(entry)); -} - -void HazelcastTestableLocalCache::putBody(const uint64_t key, const uint64_t order, - const HazelcastBodyEntry& entry) { - checkConnection(); - body_map_[orderedMapKey(key, order)] = HazelcastBodyPtr(new HazelcastBodyEntry(entry)); -} - -HazelcastHeaderPtr HazelcastTestableLocalCache::getHeader(const uint64_t key) { - checkConnection(); - auto result = header_map_.find(mapKey(key)); - if (result != header_map_.end()) { - // New objects are created during deserialization. Hence not returning the original one here. - return HazelcastHeaderPtr(new HazelcastHeaderEntry(*result->second)); - } else { - return nullptr; - } -} - -HazelcastBodyPtr HazelcastTestableLocalCache::getBody(const uint64_t key, const uint64_t order) { - checkConnection(); - auto result = body_map_.find(orderedMapKey(key, order)); - if (result != body_map_.end()) { - return HazelcastBodyPtr(new HazelcastBodyEntry(*result->second)); - } else { - return nullptr; - } -} - -void HazelcastTestableLocalCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { - if (!tryLock(key)) { - return; - } - auto header = getHeader(key); - if (header && header->version() != version) { - unlock(key); - return; - } - int body_count = body_size / body_partition_size_; - while (body_count >= 0) { - body_map_.erase(orderedMapKey(key, body_count--)); - } - header_map_.erase(mapKey(key)); - unlock(key); -} -void HazelcastTestableLocalCache::onVersionMismatch(uint64_t key, int32_t version, - uint64_t body_size) { - onMissingBody(key, version, body_size); -} - -void HazelcastTestableLocalCache::putResponseIfAbsent(const uint64_t key, - const HazelcastResponseEntry& entry) { - checkConnection(); - if (response_map_.find(mapKey(key)) != response_map_.end()) { - return; - } - response_map_[mapKey(key)] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); -} - -HazelcastResponsePtr HazelcastTestableLocalCache::getResponse(const uint64_t key) { - checkConnection(); - auto result = response_map_.find(mapKey(key)); - if (result != response_map_.end()) { - return HazelcastResponsePtr(new HazelcastResponseEntry(*result->second)); - } else { - return nullptr; - } -} - -bool HazelcastTestableLocalCache::tryLock(const uint64_t key) { - checkConnection(); - if (unified_) { - bool locked = - std::find(response_locks_.begin(), response_locks_.end(), key) != response_locks_.end(); - if (locked) { - return false; - } else { - response_locks_.push_back(key); - return true; - } - } else { - bool locked = std::find(header_locks_.begin(), header_locks_.end(), key) != header_locks_.end(); - if (locked) { - return false; - } else { - header_locks_.push_back(key); - return true; - } - } -} - -void HazelcastTestableLocalCache::unlock(const uint64_t key) { - checkConnection(); - if (unified_) { - response_locks_.erase(std::remove(response_locks_.begin(), response_locks_.end(), key), - response_locks_.end()); - } else { - header_locks_.erase(std::remove(header_locks_.begin(), header_locks_.end(), key), - header_locks_.end()); - } -} - -uint64_t HazelcastTestableLocalCache::random() { return ++random_counter_; } - -} // namespace HazelcastHttpCache -} // namespace Cache -} // namespace HttpFilters -} // namespace Extensions -} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h deleted file mode 100644 index 5c1e095c1bab1..0000000000000 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache_impl.h" - -namespace Envoy { -namespace Extensions { -namespace HttpFilters { -namespace Cache { -namespace HazelcastHttpCache { - -/** - * Test interface for cache. - */ -class HazelcastTestableHttpCache { -public: - HazelcastTestableHttpCache(HazelcastCache& cache) : cache_(cache){}; - - virtual void clearTestMaps() PURE; - virtual void dropTestConnection() PURE; - virtual void restoreTestConnection() PURE; - - virtual int headerTestMapSize() PURE; - virtual int bodyTestMapSize() PURE; - virtual int responseTestMapSize() PURE; - - virtual void putTestResponse(uint64_t key, const HazelcastResponseEntry& entry) PURE; - virtual void removeTestBody(uint64_t key, uint64_t order) PURE; - - virtual ~HazelcastTestableHttpCache() = default; - - HazelcastCache& base() { return cache_; } - -private: - HazelcastCache& cache_; -}; - -/** - * Testable cache with local storage. - * - * Does not connect to a Hazelcast cluster but instead stores entries locally and - * mimics the remote cache behavior. Running a Hazelcast cluster or a single member - * is not needed during test. - */ -class HazelcastTestableLocalCache : public HazelcastTestableHttpCache, public HazelcastCache { -public: - HazelcastTestableLocalCache(HazelcastHttpCacheConfig config); - - // HazelcastTestableHttpCache - void clearTestMaps() override; - void dropTestConnection() override; - void restoreTestConnection() override; - int headerTestMapSize() override; - int bodyTestMapSize() override; - int responseTestMapSize() override; - void putTestResponse(uint64_t key, const HazelcastResponseEntry& entry) override; - void removeTestBody(uint64_t key, uint64_t order) override; - - // HttpCache - LookupContextPtr makeLookupContext(LookupRequest&& request) override; - InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; - void updateHeaders(LookupContextPtr&& lookup_context, - Http::ResponseHeaderMapPtr&& response_headers) override; - CacheInfo cacheInfo() const override; - - // HazelcastCache - void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) override; - void putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) override; - HazelcastHeaderPtr getHeader(const uint64_t key) override; - HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) override; - void onMissingBody(uint64_t key, int32_t version, uint64_t body_size) override; - void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) override; - void putResponseIfAbsent(const uint64_t key, const HazelcastResponseEntry& entry) override; - HazelcastResponsePtr getResponse(const uint64_t key) override; - bool tryLock(const uint64_t key) override; - void unlock(const uint64_t key) override; - uint64_t random() override; - -private: - void checkConnection() { - if (!connected_) { - throw std::exception(); - } - } - - std::unordered_map header_map_; - std::unordered_map body_map_; - std::unordered_map response_map_; - - std::vector header_locks_; - std::vector response_locks_; - - bool connected_ = false; - uint32_t random_counter_ = 0; -}; - -/** - * Test wrapper for HazelcastHttpCache. - * - * Establishes a TCP connection with an alive Hazelcast member during tests and - * stores data in distributed map. Simulates the original version of the cache. - */ -class HazelcastTestableRemoteCache : public HazelcastTestableHttpCache, public HazelcastHttpCache { -public: - HazelcastTestableRemoteCache(HazelcastHttpCacheConfig config) - : HazelcastTestableHttpCache(dynamic_cast(*this)), - HazelcastHttpCache(config) {} - - void clearTestMaps() override { - if (unified_) { - getResponseMap().clear(); - } else { - getBodyMap().clear(); - getHeaderMap().clear(); - } - } - - void dropTestConnection() override { shutdown(false); } - - void restoreTestConnection() override { connect(); } - - int headerTestMapSize() override { return getHeaderMap().size(); } - - int bodyTestMapSize() override { return getBodyMap().size(); } - - int responseTestMapSize() override { return getResponseMap().size(); } - - void removeTestBody(uint64_t key, uint64_t order) override { - getBodyMap().remove(orderedMapKey(key, order)); - } - - void putTestResponse(uint64_t key, const HazelcastResponseEntry& entry) override { - getResponseMap().put(mapKey(key), entry); - } -}; - -} // namespace HazelcastHttpCache -} // namespace Cache -} // namespace HttpFilters -} // namespace Extensions -} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index ed417e5c5fbd8..18af4a01e6065 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -2,7 +2,7 @@ #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" -#include "test/extensions/filters/http/cache/hazelcast_http_cache/util.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h" namespace Envoy { namespace Extensions { @@ -16,18 +16,17 @@ namespace HazelcastHttpCache { class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(true); - // To test the cache with a real Hazelcast instance, remote cache - // must be used during tests. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); - cache_->restoreTestConnection(); - cache_->clearTestMaps(); + // To test the cache with a real Hazelcast instance, use remote test cache. + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); + cache_->start(); + cache_->getTestAccessor().clearMaps(); } }; TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedInsertionWhenMaxSizeReached) { const std::string RequestPath("/abort/when/max/size/reached"); - InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup(RequestPath)); + InsertContextPtr insert_context = cache_->makeInsertContext(lookup(RequestPath)); insert_context->insertHeaders(getResponseHeaders(), false); bool ready_for_next = true; while (ready_for_next) { @@ -81,7 +80,7 @@ TEST_F(HazelcastUnifiedCacheTest, DoNotOverrideExistingResponse) { } TEST_F(HazelcastUnifiedCacheTest, UnifiedHeaderOnlyResponse) { - InsertContextPtr insert_context = cache_->base().makeInsertContext(lookup("/header/only")); + InsertContextPtr insert_context = cache_->makeInsertContext(lookup("/header/only")); insert_context->insertHeaders(getResponseHeaders(), true); LookupContextPtr lookup_context = lookup("/header/only"); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); @@ -105,12 +104,12 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. - auto response = cache_->base().getResponse(variant_hash_key); + auto response = cache_->getResponse(variant_hash_key); Key modified = response->header().variantKey(); modified.add_custom_fields("custom1"); modified.add_custom_fields("custom2"); response->header().variantKey(std::move(modified)); - cache_->putTestResponse(variant_hash_key, *response); + cache_->getTestAccessor().putResponse(cache_->mapKey(variant_hash_key), *response); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -121,7 +120,7 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - EXPECT_EQ(1, cache_->responseTestMapSize()); + EXPECT_EQ(1, cache_->getTestAccessor().responseMapSize()); } TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { @@ -134,13 +133,36 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { lookup_context1 = lookup(RequestPath1); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); - cache_->dropTestConnection(); + // These lookup are not marked as to be aborted since connection is still alive. + // They will be used to test insertion behavior when cache is offline. + LookupContextPtr succeed_lookup1 = lookup(RequestPath1); + LookupContextPtr succeed_lookup2 = lookup(RequestPath1); + LookupContextPtr succeed_lookup3 = lookup(RequestPath1); + cache_->getTestAccessor().dropConnection(); + + // UnifiedLookupContext::getHeaders when HazelcastClientOfflineException is thrown. lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + // lookup has marked insertion to be aborted. Hence the following should do no-op. insert(move(lookup_context1), getResponseHeaders(), Body); - cache_->restoreTestConnection(); + // UnifiedLookupContext::getHeaders when OperationTimeoutException is thrown. + lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // UnifiedLookupContext::getHeaders when std::exception is thrown. + lookup_context1 = lookup(RequestPath1); + EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); + + // UnifiedInsertContext::insertResponse when HazelcastClientOfflineException is thrown. + insert(move(succeed_lookup1), getResponseHeaders(), Body); + // UnifiedInsertContext::insertResponse when OperationTimeoutException is thrown. + insert(move(succeed_lookup2), getResponseHeaders(), Body); + // UnifiedInsertContext::insertResponse when std::exception is thrown. + insert(move(succeed_lookup3), getResponseHeaders(), Body); + + cache_->getTestAccessor().restoreConnection(); lookup_context1 = lookup(RequestPath1); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h new file mode 100644 index 0000000000000..eef4ebe22447d --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h @@ -0,0 +1,248 @@ +#pragma once + +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +/** + * Abstraction for the accessors used in tests. + * + * Contains pure functions to obtain storage information, modify the storage and + * change accessor behavior directly. + */ +class TestAccessor { +public: + TestAccessor(){}; + + virtual void clearMaps() PURE; + virtual void dropConnection() PURE; + virtual void restoreConnection() PURE; + + virtual int headerMapSize() PURE; + virtual int bodyMapSize() PURE; + virtual int responseMapSize() PURE; + + virtual void putResponse(int64_t key, const HazelcastResponseEntry& entry) PURE; + virtual void removeBody(const std::string& key) PURE; + + virtual void failOnLock() PURE; + + virtual ~TestAccessor() = default; +}; + +/** + * Testable Hazelcast cluster accessor. + * + * @note A Hazelcast instance must be up during tests when this accessor is used. + */ +class RemoteTestAccessor : public TestAccessor, public HazelcastClusterAccessor { +public: + RemoteTestAccessor(ClientConfig&& client_config, const std::string& app_prefix, + const uint64_t partition_size) + : HazelcastClusterAccessor(std::move(client_config), app_prefix, partition_size){}; + + void clearMaps() override { + getResponseMap().clear(); + getBodyMap().clear(); + getHeaderMap().clear(); + } + + void dropConnection() override { disconnect(); } + + void restoreConnection() override { connect(); } + + int headerMapSize() override { return getHeaderMap().size(); } + + int bodyMapSize() override { return getBodyMap().size(); } + + int responseMapSize() override { return getResponseMap().size(); } + + void removeBody(const std::string& key) override { getBodyMap().remove(key); } + + void putResponse(int64_t key, const HazelcastResponseEntry& entry) override { + getResponseMap().put(key, entry); + } + + void failOnLock() override {} // Required for local accessor only. +}; + +/** + * Testable local storage accessor. + * + * @note This accessor does not use any Hazelcast instance during tests. + * Instead, it simulates Hazelcast instance with local storage. + */ +class LocalTestAccessor : public StorageAccessor, public TestAccessor { +public: + LocalTestAccessor() {} + + // TestAccessor + void clearMaps() override { + header_map_.clear(); + body_map_.clear(); + response_map_.clear(); + } + + void dropConnection() override { disconnect(); } + + void restoreConnection() override { connect(); } + + int headerMapSize() override { return header_map_.size(); } + + int bodyMapSize() override { return body_map_.size(); } + + int responseMapSize() override { return response_map_.size(); } + + void putResponse(int64_t key, const HazelcastResponseEntry& entry) override { + checkConnection(); + response_map_[key] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); + } + + void removeBody(const std::string& key) override { + checkConnection(); + removeBodyAsync(key); + } + + // StorageAccessor + void putHeader(const int64_t key, const HazelcastHeaderEntry& value) override { + checkConnection(); + header_map_[key] = HazelcastHeaderPtr(new HazelcastHeaderEntry(value)); + } + + void putBody(const std::string& key, const HazelcastBodyEntry& value) override { + checkConnection(); + body_map_[key] = HazelcastBodyPtr(new HazelcastBodyEntry(value)); + } + + void putResponseIfAbsent(const int64_t key, const HazelcastResponseEntry& value) override { + checkConnection(); + if (response_map_.find(key) != response_map_.end()) { + return; + } + response_map_[key] = HazelcastResponsePtr(new HazelcastResponseEntry(value)); + } + + HazelcastHeaderPtr getHeader(const int64_t key) override { + checkConnection(); + auto result = header_map_.find(key); + if (result != header_map_.end()) { + // New objects are created during deserialization. Hence not returning + // the original one here. + return HazelcastHeaderPtr(new HazelcastHeaderEntry(*result->second)); + } else { + return nullptr; + } + } + + HazelcastBodyPtr getBody(const std::string& key) override { + checkConnection(); + auto result = body_map_.find(key); + if (result != body_map_.end()) { + return HazelcastBodyPtr(new HazelcastBodyEntry(*result->second)); + } else { + return nullptr; + } + } + + HazelcastResponsePtr getResponse(const int64_t key) override { + checkConnection(); + auto result = response_map_.find(key); + if (result != response_map_.end()) { + return HazelcastResponsePtr(new HazelcastResponseEntry(*result->second)); + } else { + return nullptr; + } + } + + void removeBodyAsync(const std::string& key) override { + checkConnection(); + body_map_.erase(key); + } + + void removeHeader(const int64_t key) override { + checkConnection(); + header_map_.erase(key); + } + + bool tryLock(const int64_t key, bool unified) override { + checkConnection(fail_on_lock_); + if (unified) { + bool locked = + std::find(response_locks_.begin(), response_locks_.end(), key) != response_locks_.end(); + if (locked) { + return false; + } else { + response_locks_.push_back(key); + return true; + } + } else { + bool locked = + std::find(header_locks_.begin(), header_locks_.end(), key) != header_locks_.end(); + if (locked) { + return false; + } else { + header_locks_.push_back(key); + return true; + } + } + } + + void unlock(const int64_t key, bool unified) override { + checkConnection(); + if (unified) { + response_locks_.erase(std::remove(response_locks_.begin(), response_locks_.end(), key), + response_locks_.end()); + } else { + header_locks_.erase(std::remove(header_locks_.begin(), header_locks_.end(), key), + header_locks_.end()); + } + } + + bool isRunning() override { return connected_; } + + std::string clusterName() override { return "LocalTestAccessor"; } + + void connect() override { connected_ = true; } + + void disconnect() override { connected_ = false; } + + void failOnLock() override { fail_on_lock_ = true; } + +private: + void checkConnection(bool force_fail = false) { + if (!connected_ || force_fail) { + // Different exceptions are thrown for consecutive fails to test other catch behaviors. + switch (exception_counter_++ % 3) { + case 0: + throw std::exception(); + case 1: + throw hazelcast::client::exception::HazelcastClientOfflineException( + "LocalTestAccessor::checkConnection", "Hazelcast client is offline"); + default: + throw hazelcast::client::exception::OperationTimeoutException( + "LocalTestAccessor::checkConnection", "Operation timed out"); + } + } + } + + std::unordered_map header_map_; + std::unordered_map body_map_; + std::unordered_map response_map_; + + std::vector header_locks_; + std::vector response_locks_; + + bool connected_ = false; + bool fail_on_lock_ = false; + int exception_counter_ = 0; +}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h new file mode 100644 index 0000000000000..06dcf90892055 --- /dev/null +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h @@ -0,0 +1,80 @@ +#pragma once + +#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" + +#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace HazelcastHttpCache { + +/** + * Base testable cache for both local and remote ones. + * + * Exposes accessor to extend testing flexibility. Storage control can be achieved + * directly via test accessors. + */ +class HazelcastHttpTestCache : public HazelcastHttpCache { +public: + HazelcastHttpTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpCache(std::move(config)) {} + + TestAccessor& getTestAccessor() { return dynamic_cast(*accessor_); } +}; + +/** + * Testable cache with RemoteTestAccessor. + * + * Requires a running Hazelcast instance to be tested. + */ +class RemoteTestCache : public HazelcastHttpTestCache { +public: + RemoteTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} + + void start() override { + if (accessor_ && accessor_->isRunning()) { + return; + } + + ClientConfig client_config = ConfigUtil::getClientConfig(cache_config_); + client_config.getSerializationConfig().addDataSerializableFactory( + HazelcastCacheEntrySerializableFactory::FACTORY_ID, + boost::shared_ptr(new HazelcastCacheEntrySerializableFactory())); + + if (!accessor_) { + accessor_ = std::make_unique( + std::move(client_config), cache_config_.app_prefix(), body_partition_size_); + } + + try { + accessor_->connect(); + } catch (...) { + throw EnvoyException("Hazelcast Client could not connect to any cluster."); + } + } +}; + +/** + * Testable cache with LocalTestAccessor. + * + * Does not require a running Hazelcast instance. Instead, tests the cache + * with local storage. This is the way the cache is tested in CI environment. + */ +class LocalTestCache : public HazelcastHttpTestCache { +public: + LocalTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} + + void start() override { + if (!accessor_) { + accessor_ = std::make_unique(); + } + accessor_->connect(); + } +}; + +} // namespace HazelcastHttpCache +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h similarity index 95% rename from test/extensions/filters/http/cache/hazelcast_http_cache/util.h rename to test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h index 3113f17c068f4..ab5dced125da4 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h @@ -1,6 +1,6 @@ #pragma once -#include "test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_test_cache.h" +#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h" #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" @@ -98,14 +98,14 @@ class HazelcastHttpCacheTestBase : public testing::Test { LookupContextPtr lookup(absl::string_view request_path) { LookupRequest request = makeLookupRequest(request_path); - LookupContextPtr context = cache_->base().makeLookupContext(std::move(request)); + LookupContextPtr context = cache_->makeLookupContext(std::move(request)); context->getHeaders([this](LookupResult&& result) { lookup_result_ = std::move(result); }); return context; } void insert(LookupContextPtr lookup, const Http::TestResponseHeaderMapImpl& response_headers, const absl::string_view response_body) { - InsertContextPtr insert_context = cache_->base().makeInsertContext(move(lookup)); + InsertContextPtr insert_context = cache_->makeInsertContext(move(lookup)); insert_context->insertHeaders(response_headers, response_body == nullptr); if (response_body == nullptr) { return; @@ -150,7 +150,7 @@ class HazelcastHttpCacheTestBase : public testing::Test { return AssertionSuccess(); } - std::unique_ptr cache_; + std::unique_ptr cache_; LookupResult lookup_result_; Http::TestRequestHeaderMapImpl request_headers_; Event::SimulatedTimeSystem time_source_; From fde9e19f73d1571ae559320e0be45eed6f2a7977 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Wed, 20 May 2020 11:42:34 +0300 Subject: [PATCH 14/33] Change key serialization from UTF to byte array. Signed-off-by: Enes Ozcan --- .../cache/hazelcast_http_cache/hazelcast_cache_entry.cc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc index e6876974441e4..4007de9751a60 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.cc @@ -44,7 +44,10 @@ void HazelcastHeaderEntry::writeUnifiedData(ObjectDataOutput& writer) const { &writer); std::string serialized; variant_key_.SerializeToString(&serialized); - writer.writeUTF(&serialized); + // Serialized bytes are binary, not text. String is used as a container only + // during protobuf serialization. Hence ObjectDataOutput::writeUTF might fail. + std::vector bytes(serialized.begin(), serialized.end()); + writer.writeByteArray(&bytes); } void HazelcastHeaderEntry::readUnifiedData(ObjectDataInput& reader) { @@ -58,8 +61,8 @@ void HazelcastHeaderEntry::readUnifiedData(ObjectDataInput& reader) { val.append(val_vector.data(), val_vector.size()); header_map_->addViaMove(std::move(key), std::move(val)); } - std::string serialized = *reader.readUTF(); - variant_key_.ParseFromString(serialized); + std::vector bytes = *reader.readByteArray(); + variant_key_.ParseFromString(std::string(reinterpret_cast(bytes.data()), bytes.size())); } HazelcastHeaderEntry::HazelcastHeaderEntry() = default; From 104f8e0af09d6627127e87a6869bc17f87231e59 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Wed, 20 May 2020 12:04:04 +0300 Subject: [PATCH 15/33] Update plugin doc. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_cache_plugin.md | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/source/docs/filters/http/cache/hazelcast_cache_plugin.md b/source/docs/filters/http/cache/hazelcast_cache_plugin.md index 5e5b30ea3090d..b61395e4e2219 100644 --- a/source/docs/filters/http/cache/hazelcast_cache_plugin.md +++ b/source/docs/filters/http/cache/hazelcast_cache_plugin.md @@ -1,8 +1,8 @@ ### Hazelcast Http Cache Plugin Work in Progress--Cache filter has not implemented features. The corresponding ones are not ready for the plugin too. -Hazelcast Http Cache provides a pluggable storage implementation backed by Hazelcast In Memory Data Grid for the http -cache filter. Using Hazelcast C++ client, the plugin does not store any http response locally but in a distributed map +Hazelcast Http Cache provides a pluggable storage implementation backed by Hazelcast In Memory Data Grid for the Http +cache filter. Using Hazelcast C++ client, the plugin does not store any Http response locally but in a distributed map provided by Hazelcast cluster. To enable the cache plugin, the network configuration belongs to a cluster to be connected must be set on the cache plugin configuration. @@ -10,13 +10,13 @@ connected must be set on the cache plugin configuration. The plugin comes with two modes: - **Unified** -A cached http response is stored as a single entry in the cache. On a range http request, regardless of the requested +A cached Http response is stored as a single entry in the cache. On a range Http request, regardless of the requested range, the whole response body is fetched from the cache and then only the desired bytes are served along with the headers and trailers (if any). This mode is handy where response body sizes are reasonably large (up to 10 KB), or range requests are not frequent, or they are not allowed at all. - **Divided** -A cached http response is stored as multiple entries in the cache. Two separate maps are used to store a single +A cached Http response is stored as multiple entries in the cache. Two separate maps are used to store a single response. In one of them, response headers, body size, and trailers (if any) are stored. In the other one, the corresponding response body is stored in multiple entries each of which has a certain size configured via `partition size` in the plugin configuration. That is, for a response of size 50 KB, if the configured partition size is 20 KB, @@ -40,8 +40,8 @@ as a sidecar to Envoy, form up a cluster using Hazelcast Kubernetes plugin, etc. will be the addresses and ports of the cluster members and the group information of the cluster. Providing the address of only one member in the cluster will be enough for the connection but using more than one is recommended. -Related links: [Hazelcast Docker Hub](https://hub.docker.com/r/hazelcast/hazelcast/), [Hazelcast Kubernetes Plugin] -(https://github.com/hazelcast/hazelcast-kubernetes) +Related links: [Hazelcast Docker Hub](https://hub.docker.com/r/hazelcast/hazelcast/), +[Hazelcast Kubernetes Plugin](https://github.com/hazelcast/hazelcast-kubernetes) ## Configuring Hazelcast cluster for the cache Eviction, maximum size, and other related properties for the cache must be configured on the server-side @@ -55,12 +55,14 @@ via programmatic configuration or `hazelcast.xml`. 1000 @@ -87,18 +89,19 @@ via programmatic configuration or `hazelcast.xml`. - **Divided Mode** ```xml - + 0 - 25 - LRU 195 HazelcastHeaderEntry(response headers) +// --> HazelcastHeaderEntry(response headers) // -// --> HazelcastBodyEntry(0-2 KB) -// --> HazelcastBodyEntry(2-4 KB) -// --> HazelcastBodyEntry(4-5 KB) +// --> HazelcastBodyEntry(0-2 KB) +// --> HazelcastBodyEntry(2-4 KB) +// --> HazelcastBodyEntry(4-5 KB) // void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) { ASSERT(range.end() <= total_body_size_); @@ -281,10 +281,10 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal // Lookup for only one body partition which includes the range.begin(). uint64_t body_index = range.begin() / body_partition_size_; HazelcastBodyPtr body; - ENVOY_LOG(debug, "Looking up divided body with key: {}u, order: {}", variant_hash_key_, + ENVOY_LOG(debug, "Looking up divided body with key hash: {}u, order: {}", variant_key_hash_, body_index); try { - body = hz_cache_.getBody(variant_hash_key_, body_index); + body = hz_cache_.getBody(variant_key_hash_, body_index); } catch (HazelcastClientOfflineException& e) { handleBodyLookupFailure("Hazelcast cluster connection is lost! Aborting lookups and " "insertions until the connection is restored...", @@ -301,13 +301,13 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal if (body) { ENVOY_LOG(debug, "Found divided body: [key: {}u + \"#{}\", version: {}, size: {}]", - variant_hash_key_, body_index, body->version(), body->length()); + variant_key_hash_, body_index, body->version(), body->length()); if (body->version() != version_) { - hz_cache_.onVersionMismatch(variant_hash_key_, version_, total_body_size_); + hz_cache_.onVersionMismatch(variant_key_hash_, version_, total_body_size_); handleBodyLookupFailure( fmt::format("Body version mismatched with header for " "key {}u at body: {}. Aborting lookup and performing cleanup.", - variant_hash_key_, body_index), + variant_key_hash_, body_index), cb, false); return; } @@ -324,10 +324,10 @@ void DividedLookupContext::getBody(const AdjustedByteRange& range, LookupBodyCal } } else { // Body partition is expected to reside in the cache but lookup is failed. - hz_cache_.onMissingBody(variant_hash_key_, version_, total_body_size_); + hz_cache_.onMissingBody(variant_key_hash_, version_, total_body_size_); handleBodyLookupFailure(fmt::format("Found missing body for key {}u at index: {}. Response " "with body size {} has been cleaned up from the cache.", - variant_hash_key_, body_index, total_body_size_), + variant_key_hash_, body_index, total_body_size_), cb, false); } }; @@ -352,7 +352,7 @@ DividedInsertContext::DividedInsertContext(LookupContext& lookup_context, Hazelc // will be responsible for the insertion. This is also valid when multiple cache // filters from different proxies are connected to the same Hazelcast cluster. // There is no such a mechanism for UNIFIED mode. - insertion_allowed_ = hz_cache_.tryLock(variant_hash_key_); + insertion_allowed_ = hz_cache_.tryLock(variant_key_hash_); return; } catch (HazelcastClientOfflineException& e) { ENVOY_LOG(warn, "Hazelcast cluster connection is lost! Aborting lookups and insertions until " @@ -384,7 +384,7 @@ void DividedInsertContext::insertHeaders(const Http::ResponseHeaderMap& response void DividedInsertContext::insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, bool end_stream) { if (abort_insertion_ || !insertion_allowed_) { - ENVOY_LOG(debug, "Skipping insertion for the hash key: {}u", variant_hash_key_); + ENVOY_LOG(debug, "Skipping insertion for the hash key: {}u", variant_key_hash_); if (ready_for_next_chunk) { ready_for_next_chunk(false); } @@ -452,7 +452,7 @@ bool DividedInsertContext::flushBuffer() { HazelcastBodyEntry bodyEntry(std::move(buffer_vector_), version_); buffer_vector_.clear(); try { - hz_cache_.putBody(variant_hash_key_, body_order_++, bodyEntry); + hz_cache_.putBody(variant_key_hash_, body_order_++, bodyEntry); } catch (HazelcastClientOfflineException& e) { ENVOY_LOG(warn, "Hazelcast cluster connection is lost!"); return false; @@ -481,9 +481,9 @@ void DividedInsertContext::insertHeader() { HazelcastHeaderEntry header(std::move(header_map_), std::move(variant_key_), total_body_size_, version_); try { - hz_cache_.putHeader(variant_hash_key_, header); - hz_cache_.unlock(variant_hash_key_); - ENVOY_LOG(debug, "Inserted header entry with key {}u", variant_hash_key_); + hz_cache_.putHeader(variant_key_hash_, header); + hz_cache_.unlock(variant_key_hash_); + ENVOY_LOG(debug, "Inserted header entry with key {}u", variant_key_hash_); // To handle leftover locks in a failure, hazelcast.lock.max.lease.time.seconds property // must be set to a reasonable value on the server side. It is Long.MAX by default. // To make this independent from the server configuration, tryLock with leaseTime diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h index 08e7be1ca18b3..1ac51bf753874 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h @@ -20,7 +20,7 @@ class HazelcastLookupContextBase : public LookupContext, void getTrailers(LookupTrailersCallback&&) override; const Key& variantKey() const { return lookup_request_.key(); } - uint64_t variantHashKey() const { return variant_hash_key_; } + uint64_t variantKeyHash() const { return variant_key_hash_; } bool isAborted() const { return abort_insertion_; } protected: @@ -32,7 +32,7 @@ class HazelcastLookupContextBase : public LookupContext, /** Hash key aware of vary headers. Lookup to header and response entry is performed using this * key. */ - uint64_t variant_hash_key_; + uint64_t variant_key_hash_; /** Flag to notice insert context created for this lookup */ bool abort_insertion_ = false; @@ -47,7 +47,7 @@ class HazelcastLookupContextBase : public LookupContext, * headers. Rather than storing multiple responses with the same key and * then querying them according to vary headers, a different key for each * response including vary headers in custom fields is created here. Hence - * responses can be found by their directly without querying. + * responses can be found by their directly without querying. * * @param raw_key Key to be modified created by the filter. */ @@ -84,7 +84,7 @@ class HazelcastInsertContextBase : public InsertContext, bool committed_end_stream_ = false; // Derived from lookup context - const uint64_t variant_hash_key_; + const uint64_t variant_key_hash_; Key variant_key_; const bool abort_insertion_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index 501843a5f519c..c502a5b108145 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -19,27 +19,28 @@ HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) max_body_size_(ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())), cache_config_(config) {} -void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t body_size) { +void HazelcastHttpCache::onMissingBody(uint64_t key_hash, int32_t version, uint64_t body_size) { try { - if (!tryLock(key)) { - // Let lock owner context to recover it. + if (!tryLock(key_hash)) { + // If multiple onMissingBody calls are made for the same key hash simultaneously, + // the locking here will allow only one of them to perform clean up. return; } - auto header = getHeader(key); + auto header = getHeader(key_hash); if (header && header->version() != version) { // The missed body does not belong to the looked up header. Probably eviction and then // insertion for the header has happened in the meantime. Since new insertion will // override the existing bodies, ignore the cleanup and let orphan bodies (belong to // evicted header, not overridden) be evicted by TTL as well. - unlock(key); + unlock(key_hash); return; } int body_count = body_size / body_partition_size_; while (body_count >= 0) { - accessor_->removeBodyAsync(orderedMapKey(key, body_count--)); + accessor_->removeBodyAsync(orderedMapKey(key_hash, body_count--)); } - accessor_->removeHeader(mapKey(key)); - unlock(key); + accessor_->removeHeader(mapKey(key_hash)); + unlock(key_hash); } catch (HazelcastClientOfflineException& e) { // see DividedInsertContext::insertHeader for left over locks on a connection failure. ENVOY_LOG(warn, "Hazelcast Connection is offline!"); @@ -48,8 +49,8 @@ void HazelcastHttpCache::onMissingBody(uint64_t key, int32_t version, uint64_t b } } -void HazelcastHttpCache::onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size) { - onMissingBody(key, version, body_size); +void HazelcastHttpCache::onVersionMismatch(uint64_t key_hash, int32_t version, uint64_t body_size) { + onMissingBody(key_hash, version, body_size); } void HazelcastHttpCache::start() { @@ -125,7 +126,7 @@ InsertContextPtr HazelcastHttpCache::makeInsertContext(LookupContextPtr&& lookup // TODO(enozcan): Implement when it's ready on the filter side. // Depending on the filter's implementation, the cached entry's // variant_key_ must be updated as well. Also, if vary headers -// change then the hash key of the response will change and +// change then the key hash of the response will change and // updating only header map will not be enough in this case. void HazelcastHttpCache::updateHeaders(LookupContextPtr&&, Http::ResponseHeaderMapPtr&&) { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; @@ -159,14 +160,14 @@ HttpCache& HazelcastHttpCacheFactory::getCache( return *cache_; } -HazelcastHttpCache* HazelcastHttpCacheFactory::getOfflineCache( +HazelcastHttpCachePtr HazelcastHttpCacheFactory::getOfflineCache( const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { if (!cache_) { HazelcastHttpCacheConfig hz_cache_config; MessageUtil::unpackTo(config.typed_config(), hz_cache_config); cache_ = std::make_unique(hz_cache_config); } - return cache_.release(); + return std::move(cache_); } static Registry::RegisterFactory register_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 07343850bd306..1d594988b4abe 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -27,7 +27,7 @@ using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; * Supports two cache modes: UNIFIED and DIVIDED. * * In UNIFIED mode, an HTTP response is wrapped by a HazelcastResponseEntry - * with its all fields (headers, body, trailers, request key) and stored in + * with all its fields (headers, body, trailers, request key) and stored in * distributed map. On a range HTTP request, regardless of the requested * range, the whole response body is fetched from the cache. * @@ -48,43 +48,44 @@ class HazelcastHttpCache : public HttpCache, /** * Puts a header entry into header cache. - * @param key Hash key for the entry - * @param entry Entry to be inserted - * @note Generated keys should be consistent across restarts, architectures, - * builds, and configurations. Otherwise, different filters using the - * same Hazelcast cluster might store the same response with different - * keys. + * @param key_hash Hash of the filter's cache key + * @param entry Entry to be inserted + * @note Generated hashes should be consistent across restarts, architectures, + * builds, and configurations. Otherwise, different filters using the + * same Hazelcast cluster might store the same response with different + * hashes. */ - void putHeader(const uint64_t key, const HazelcastHeaderEntry& entry) { - accessor_->putHeader(mapKey(key), entry); + void putHeader(const uint64_t key_hash, const HazelcastHeaderEntry& entry) { + accessor_->putHeader(mapKey(key_hash), entry); } /** * Puts a body entry into body cache. - * @param key Hash key for the whole body derived from the header - * @param order Order of the body chunk among other partitions starting from 0 - * @param entry Entry to be inserted - * @note The key for a body partition must be obtainable from its header key. + * @param key_hash Hash of the filter's cache key + * @param order Order of the body chunk among other partitions starting from 0 + * @param entry Entry to be inserted + * @note The map key of a body partition must be obtainable from its header's + * key hash. */ - void putBody(const uint64_t key, const uint64_t order, const HazelcastBodyEntry& entry) { - accessor_->putBody(orderedMapKey(key, order), entry); + void putBody(const uint64_t key_hash, const uint64_t order, const HazelcastBodyEntry& entry) { + accessor_->putBody(orderedMapKey(key_hash, order), entry); } /** - * Performs a lookup to header cache for the given key. - * @param key Hash key for the entry - * @return HazelcastHeaderPtr to cached entry if found, nullptr otherwise + * Performs a lookup to header cache for the given key hash. + * @param key_hash Hash of the filter's cache key + * @return HazelcastHeaderPtr to cached entry if found, nullptr otherwise */ - HazelcastHeaderPtr getHeader(const uint64_t key) { return accessor_->getHeader(mapKey(key)); } + HazelcastHeaderPtr getHeader(const uint64_t key_hash) { return accessor_->getHeader(mapKey(key_hash)); } /** - * Performs a lookup to body cache for the given key and order pair. - * @param key Hash key for the whole body - * @param order Order of the body chunk among other partitions - * @return HazelcastBodyPtr to cached entry if found, nullptr otherwise + * Performs a lookup to body cache for the given key hash and order pair. + * @param key_hash Hash of the filter's cache key + * @param order Order of the body chunk among other partitions + * @return HazelcastBodyPtr to cached entry if found, nullptr otherwise */ - HazelcastBodyPtr getBody(const uint64_t key, const uint64_t order) { - return accessor_->getBody(orderedMapKey(key, order)); + HazelcastBodyPtr getBody(const uint64_t key_hash, const uint64_t order) { + return accessor_->getBody(orderedMapKey(key_hash, order)); } /** @@ -92,39 +93,39 @@ class HazelcastHttpCache : public HttpCache, * during lookup. The header for the response is removed to make a new insertion * available by an insert context and the remaining body partitions are removed * to prevent orphan body entries stay in the cache. - * @param key Header key for the response - * @param version Version for the key and body - * @param body_size Total body size for the response + * @param key_hash Hash of the filter's cache key + * @param version Version of the header and body + * @param body_size Total body size of the response */ - void onMissingBody(uint64_t key, int32_t version, uint64_t body_size); + void onMissingBody(uint64_t key_hash, int32_t version, uint64_t body_size); /** * Cleans up a malformed response when a body partition with different version * than the header is encountered during lookup. - * @param key Header key for the response - * @param version Version for the key and body - * @param body_size Total body size for the response + * @param key_hash Hash of the filter's cache key + * @param version Version of the header and body + * @param body_size Total body size of the response */ - void onVersionMismatch(uint64_t key, int32_t version, uint64_t body_size); + void onVersionMismatch(uint64_t key_hash, int32_t version, uint64_t body_size); /// Unified mode /** * Puts a unified entry into unified cache. - * @param key Hash key for the entry - * @param entry Entry to be inserted + * @param key_hash Hash of the filter's cache key + * @param entry Entry to be inserted */ - void putResponse(const uint64_t key, const HazelcastResponseEntry& entry) { - accessor_->putResponse(mapKey(key), entry); + void putResponse(const uint64_t key_hash, const HazelcastResponseEntry& entry) { + accessor_->putResponse(mapKey(key_hash), entry); } /** - * Performs a lookup to unified cache for the given key. - * @param key Hash key for the entry - * @return HazelcastResponsePtr to cached entry if found, nullptr otherwise + * Performs a lookup to unified cache for the given key hash. + * @param key_hash Hash of the filter's cache key + * @return HazelcastResponsePtr to cached entry if found, nullptr otherwise */ - HazelcastResponsePtr getResponse(const uint64_t key) { - return accessor_->getResponse(mapKey(key)); + HazelcastResponsePtr getResponse(const uint64_t key_hash) { + return accessor_->getResponse(mapKey(key_hash)); } /// Common @@ -133,18 +134,18 @@ class HazelcastHttpCache : public HttpCache, * Attempts to lock the given key in the cache. When a key is locked, a lookup * can be performed but an insertion or update for the key must be prevented * for threads other than the lock holder. - * @param key Key to be locked - * @return True if acquired, false otherwise - * @note Used to prevent multiple insertions or updates by different - * contexts at a time. + * @param key_hash Hash of the filter's cache key + * @return True if acquired, false otherwise + * @note Used to prevent multiple insertions or updates of the same + * response by different contexts at a time. */ - bool tryLock(const uint64_t key) { return accessor_->tryLock(mapKey(key), unified_); } + bool tryLock(const uint64_t key_hash) { return accessor_->tryLock(mapKey(key_hash), unified_); } /** - * Releases the lock for the key. - * @param Key to be unlocked + * Releases the lock for the key hash. + * @param key_hash Hash of the filter's cache key */ - void unlock(const uint64_t key) { accessor_->unlock(mapKey(key), unified_); } + void unlock(const uint64_t key_hash) { accessor_->unlock(mapKey(key_hash), unified_); } /** * Produces a random number. @@ -158,7 +159,7 @@ class HazelcastHttpCache : public HttpCache, * @return Size in bytes for a single body entry configured for the cache * @note Ignored in UNIFIED mode. */ - uint64_t bodySizePerEntry() { return body_partition_size_; } + uint64_t bodySizePerEntry() const { return body_partition_size_; } /** * @return Allowed max size in bytes for a response configured for the cache @@ -166,40 +167,11 @@ class HazelcastHttpCache : public HttpCache, * than this limit, the first max_body_size_ bytes of the response * will be cached only. */ - uint64_t maxBodySize() { return max_body_size_; } - - /** - * Generates a unique signed key for an unsigned one. - * @param unsigned_key Unsigned hash key - * @return Signed unique key - * @note Hazelcast client accepts signed map keys only. - */ - inline int64_t mapKey(const uint64_t unsigned_key) { - // The reason for not static casting directly is a possible overflow - // for int64 on intermediate step for -2^63. - int64_t signed_key; - std::memcpy(&signed_key, &unsigned_key, sizeof(int64_t)); - return signed_key; - } - - /** - * Creates string keys for body partition entries obtainable from their header - * keys. - * @param key Unsigned hash key for the header - * @param order Order of the body among other partitions starting from 0 - * @return Body partition key unique for header and order pair - * @note Appending '#' or any other marker between the key and order - * string is required. Otherwise, for instance, the 11th order - * body for key 1 and the 1st order body for key 11 will have - * the same map key "111". - */ - inline std::string orderedMapKey(const uint64_t key, const uint64_t order) { - return std::to_string(key).append("#").append(std::to_string(order)); - } + uint64_t maxBodySize() const { return max_body_size_; } /** * Makes the cache ready to serve. Storage accessor connection must be established - * via StorageAccessor#connect() when the cache is started. + * via StorageAccessor::connect() when the cache is started. * * @note Keeping this virtual allows tests to override access strategy. * Using a local accessor will make the cache behavior testable without @@ -223,6 +195,41 @@ class HazelcastHttpCache : public HttpCache, ~HazelcastHttpCache() override; protected: + std::unique_ptr accessor_; + +private: + friend class HazelcastHttpCacheTestBase; + friend class HazelcastRemoteTestCache; + + /** + * Generates a Hazelcast map key from the hash of the filter's cache key. + * @param key_hash Hash of the filter's cache key + * @return Hazelcast map key + * @note Hazelcast client accepts signed map keys only. + */ + int64_t mapKey(const uint64_t key_hash) { + // The reason for not static casting directly is a possible overflow + // for int64 on intermediate step for -2^63. + int64_t signed_key; + std::memcpy(&signed_key, &key_hash, sizeof(int64_t)); + return signed_key; + } + + /** + * Creates string keys for body partition entries obtainable from the hash of the + * filter's cache key. + * @param key_hash Hash of the filter's cache key + * @param order Order of the body among other partitions starting from 0 + * @return Hazelcast map key for body entry + * @note Appending '#' or any other marker between the key and order + * string is required. Otherwise, for instance, the 11th order + * body for key 1 and the 1st order body for key 11 will have + * the same map key "111". + */ + std::string orderedMapKey(const uint64_t key_hash, const uint64_t order) { + return std::to_string(key_hash).append("#").append(std::to_string(order)); + } + /** Cache mode */ const bool unified_; @@ -232,15 +239,14 @@ class HazelcastHttpCache : public HttpCache, /** Allowed max body size in bytes for a response */ const uint64_t max_body_size_; - /** Storage access */ - std::unique_ptr accessor_; - /** typed config from CacheConfig */ HazelcastHttpCacheConfig cache_config_; Runtime::RandomGeneratorImpl rand_; }; +using HazelcastHttpCachePtr = std::unique_ptr; + class HazelcastHttpCacheFactory : public HttpCacheFactory { public: // UntypedFactory @@ -253,11 +259,11 @@ class HazelcastHttpCacheFactory : public HttpCacheFactory { HttpCache& getCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) override; - HazelcastHttpCache* // For testing only. + HazelcastHttpCachePtr // For testing only. getOfflineCache(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config); private: - std::unique_ptr cache_; + HazelcastHttpCachePtr cache_; }; } // namespace HazelcastHttpCache diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc index c7ec60428f4e2..1bed74c68c011 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc @@ -8,34 +8,34 @@ namespace HttpFilters { namespace Cache { namespace HazelcastHttpCache { -void HazelcastClusterAccessor::putHeader(const int64_t key, const HazelcastHeaderEntry& value) { - getHeaderMap().set(key, value); +void HazelcastClusterAccessor::putHeader(const int64_t map_key, const HazelcastHeaderEntry& value) { + getHeaderMap().set(map_key, value); } -void HazelcastClusterAccessor::putBody(const std::string& key, const HazelcastBodyEntry& value) { - getBodyMap().set(key, value); +void HazelcastClusterAccessor::putBody(const std::string& map_key, const HazelcastBodyEntry& value) { + getBodyMap().set(map_key, value); } -HazelcastHeaderPtr HazelcastClusterAccessor::getHeader(const int64_t key) { - return getHeaderMap().get(key); +HazelcastHeaderPtr HazelcastClusterAccessor::getHeader(const int64_t map_key) { + return getHeaderMap().get(map_key); } -HazelcastBodyPtr HazelcastClusterAccessor::getBody(const std::string& key) { - return getBodyMap().get(key); +HazelcastBodyPtr HazelcastClusterAccessor::getBody(const std::string& map_key) { + return getBodyMap().get(map_key); } -void HazelcastClusterAccessor::removeBodyAsync(const std::string& key) { - getBodyMap().removeAsync(key); +void HazelcastClusterAccessor::removeBodyAsync(const std::string& map_key) { + getBodyMap().removeAsync(map_key); } -void HazelcastClusterAccessor::removeHeader(const int64_t key) { getHeaderMap().deleteEntry(key); } +void HazelcastClusterAccessor::removeHeader(const int64_t map_key) { getHeaderMap().deleteEntry(map_key); } -void HazelcastClusterAccessor::putResponse(const int64_t key, const HazelcastResponseEntry& value) { - getResponseMap().set(key, value); +void HazelcastClusterAccessor::putResponse(const int64_t map_key, const HazelcastResponseEntry& value) { + getResponseMap().set(map_key, value); } -HazelcastResponsePtr HazelcastClusterAccessor::getResponse(const int64_t key) { - return getResponseMap().get(key); +HazelcastResponsePtr HazelcastClusterAccessor::getResponse(const int64_t map_key) { + return getResponseMap().get(map_key); } // Internal lock mechanism of Hazelcast specific to map and key pair is @@ -45,18 +45,18 @@ HazelcastResponsePtr HazelcastClusterAccessor::getResponse(const int64_t key) { // proxies when they connect to the same Hazelcast cluster. The locks used // here are re-entrant. A locked key can be acquired by the same thread // again and again based on its pid. -bool HazelcastClusterAccessor::tryLock(const int64_t key, bool unified) { - return unified ? getResponseMap().tryLock(key) : getHeaderMap().tryLock(key); +bool HazelcastClusterAccessor::tryLock(const int64_t map_key, bool unified) { + return unified ? getResponseMap().tryLock(map_key) : getHeaderMap().tryLock(map_key); } // Hazelcast does not allow a thread to unlock a key unless it's the key // owner. To handle this, IMap::forceUnlock is called here to make sure // the lock is released certainly. -void HazelcastClusterAccessor::unlock(const int64_t key, bool unified) { +void HazelcastClusterAccessor::unlock(const int64_t map_key, bool unified) { if (unified) { - getResponseMap().forceUnlock(key); + getResponseMap().forceUnlock(map_key); } else { - getHeaderMap().forceUnlock(key); + getHeaderMap().forceUnlock(map_key); } } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h index 11e9258b82796..1af7bf2d5ad17 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h @@ -23,19 +23,19 @@ class StorageAccessor { public: StorageAccessor() = default; - virtual void putHeader(const int64_t key, const HazelcastHeaderEntry& value) PURE; - virtual void putBody(const std::string& key, const HazelcastBodyEntry& value) PURE; - virtual void putResponse(const int64_t key, const HazelcastResponseEntry& value) PURE; + virtual void putHeader(const int64_t map_key, const HazelcastHeaderEntry& value) PURE; + virtual void putBody(const std::string& map_key, const HazelcastBodyEntry& value) PURE; + virtual void putResponse(const int64_t map_key, const HazelcastResponseEntry& value) PURE; - virtual HazelcastHeaderPtr getHeader(const int64_t key) PURE; - virtual HazelcastBodyPtr getBody(const std::string& key) PURE; - virtual HazelcastResponsePtr getResponse(const int64_t key) PURE; + virtual HazelcastHeaderPtr getHeader(const int64_t map_key) PURE; + virtual HazelcastBodyPtr getBody(const std::string& map_key) PURE; + virtual HazelcastResponsePtr getResponse(const int64_t map_key) PURE; - virtual void removeBodyAsync(const std::string& key) PURE; - virtual void removeHeader(const int64_t key) PURE; + virtual void removeBodyAsync(const std::string& map_key) PURE; + virtual void removeHeader(const int64_t map_key) PURE; - virtual bool tryLock(const int64_t key, bool unified) PURE; - virtual void unlock(const int64_t key, bool unified) PURE; + virtual bool tryLock(const int64_t map_key, bool unified) PURE; + virtual void unlock(const int64_t map_key, bool unified) PURE; virtual bool isRunning() PURE; virtual std::string clusterName() PURE; @@ -59,19 +59,19 @@ class HazelcastClusterAccessor : public StorageAccessor { HazelcastClusterAccessor(HazelcastHttpCache& cache, ClientConfig&& client_config, const std::string& app_prefix, const uint64_t partition_size); - void putHeader(const int64_t key, const HazelcastHeaderEntry& value) override; - void putBody(const std::string& key, const HazelcastBodyEntry& value) override; - void putResponse(const int64_t key, const HazelcastResponseEntry& value) override; + void putHeader(const int64_t map_key, const HazelcastHeaderEntry& value) override; + void putBody(const std::string& map_key, const HazelcastBodyEntry& value) override; + void putResponse(const int64_t map_key, const HazelcastResponseEntry& value) override; - HazelcastHeaderPtr getHeader(const int64_t key) override; - HazelcastBodyPtr getBody(const std::string& key) override; - HazelcastResponsePtr getResponse(const int64_t key) override; + HazelcastHeaderPtr getHeader(const int64_t map_key) override; + HazelcastBodyPtr getBody(const std::string& map_key) override; + HazelcastResponsePtr getResponse(const int64_t map_key) override; - void removeBodyAsync(const std::string& key) override; - void removeHeader(const int64_t key) override; + void removeBodyAsync(const std::string& map_key) override; + void removeHeader(const int64_t map_key) override; - bool tryLock(const int64_t key, bool unified) override; - void unlock(const int64_t key, bool unified) override; + bool tryLock(const int64_t map_key, bool unified) override; + void unlock(const int64_t map_key, bool unified) override; bool isRunning() override; std::string clusterName() override; @@ -99,7 +99,7 @@ class HazelcastClusterAccessor : public StorageAccessor { std::string constructMapName(const std::string& postfix, bool unified); /** Returns remote header cache proxy */ - inline IMap getHeaderMap() { + IMap getHeaderMap() { if (!hazelcast_client_) { throw EnvoyException("Hazelcast Client is not connected to a cluster."); } @@ -107,7 +107,7 @@ class HazelcastClusterAccessor : public StorageAccessor { } /** Returns remote body cache proxy */ - inline IMap getBodyMap() { + IMap getBodyMap() { if (!hazelcast_client_) { throw EnvoyException("Hazelcast Client is not connected to a cluster."); } @@ -115,7 +115,7 @@ class HazelcastClusterAccessor : public StorageAccessor { } /** Returns remote response cache proxy */ - inline IMap getResponseMap() { + IMap getResponseMap() { if (!hazelcast_client_) { throw EnvoyException("Hazelcast Client is not connected to a cluster."); } diff --git a/source/extensions/filters/http/cache/http_cache.h b/source/extensions/filters/http/cache/http_cache.h index 761d66be732af..32131f76ba5a6 100644 --- a/source/extensions/filters/http/cache/http_cache.h +++ b/source/extensions/filters/http/cache/http_cache.h @@ -172,7 +172,6 @@ class LookupRequest { // storage implementation forwards lookup requests to a remote cache server that supports *vary* // headers, that server may need to see these headers. For local implementations, it may be // simpler to instead call makeLookupResult with each potential response. - HeaderVector& varyHeaders() { return vary_headers_; } const HeaderVector& varyHeaders() const { return vary_headers_; } // Time when this LookupRequest was created (in response to an HTTP request). diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 4e3af89b54db7..4f39beed6e1c4 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -19,8 +19,8 @@ class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(GetParam()); // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); cache_->start(); cache_->getTestAccessor().clearMaps(); } @@ -145,13 +145,13 @@ TEST_P(HazelcastHttpCacheTest, PrivateResponse) { TEST_P(HazelcastHttpCacheTest, Miss) { LookupContextPtr name_lookup_context = lookup("/no/such/entry"); - uint64_t variant_hash_key = - static_cast(*name_lookup_context).variantHashKey(); + uint64_t variant_key_hash = + static_cast(*name_lookup_context).variantKeyHash(); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // Do not left over a missed lookup without inserting or releasing its lock. - // This is required for RemoteTestCache. - cache_->unlock(variant_hash_key); + // This is required for HazelcastRemoteTestCache. + cache_->unlock(variant_key_hash); } TEST_P(HazelcastHttpCacheTest, Fresh) { @@ -226,13 +226,13 @@ TEST(Registration, GetFactory) { { // getOfflineCache() call is for testing. It creates a HazelcastHttpCache but does // not make it operational until a start() call. This is required to make cacheInfo() - // behavior testable when using LocalTestCache. - HazelcastHttpCache* cache = + // behavior testable when using HazelcastLocalTestCache. + HazelcastHttpCachePtr cache = static_cast(factory)->getOfflineCache(config); EXPECT_EQ(cache->cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); EXPECT_THROW_WITH_MESSAGE(cache->start(), EnvoyException, "Hazelcast Client could not connect to any cluster."); - delete cache; + cache.reset(); } EXPECT_THROW_WITH_MESSAGE(factory->getCache(config), EnvoyException, "Hazelcast Client could not connect to any cluster."); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 37ef73373ac4b..33aa2a55bcfb0 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -18,8 +18,8 @@ class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(false); // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); cache_->start(); cache_->getTestAccessor().clearMaps(); } @@ -68,16 +68,16 @@ TEST_F(HazelcastDividedCacheTest, AllowOverridingCacheEntries) { TEST_F(HazelcastDividedCacheTest, CleanBodyOnHeaderEviction) { LookupContextPtr lookup_context = lookup("/header/eviction/"); - uint64_t variant_hash_key = - static_cast(*lookup_context).variantHashKey(); + uint64_t variant_key_hash = + static_cast(*lookup_context).variantKeyHash(); const int BodyCount = 3; insert(move(lookup_context), getResponseHeaders(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE * BodyCount, 'h')); EXPECT_EQ(1, cache_->getTestAccessor().headerMapSize()); EXPECT_EQ(BodyCount, cache_->getTestAccessor().bodyMapSize()); - auto keyObject = std::make_unique(cache_->mapKey(variant_hash_key)); - auto valueObject = cache_->getHeader(variant_hash_key); + auto keyObject = std::make_unique(mapKey(variant_key_hash)); + auto valueObject = cache_->getHeader(variant_key_hash); EXPECT_NE(nullptr, valueObject); Member m; @@ -143,18 +143,18 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { LookupContextPtr lookup_context = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - uint64_t variant_hash_key = - static_cast(*lookup_context).variantHashKey(); + uint64_t variant_key_hash = + static_cast(*lookup_context).variantKeyHash(); const std::string Body(HazelcastTestUtil::TEST_PARTITION_SIZE * 2, 'h'); insert(move(lookup_context), getResponseHeaders(), Body); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath1).get(), Body)); // Change version of the second partition. - auto body2 = cache_->getBody(variant_hash_key, 1); + auto body2 = cache_->getBody(variant_key_hash, 1); EXPECT_NE(body2, nullptr); body2->version(body2->version() + 1); - cache_->putBody(variant_hash_key, 1, *body2); + cache_->putBody(variant_key_hash, 1, *body2); // Change happened in the second partition. Lookup to the first one should be successful. lookup_context = lookup(RequestPath1); @@ -176,8 +176,8 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - uint64_t variant_hash_key = - static_cast(*lookup_context).variantHashKey(); + uint64_t variant_key_hash = + static_cast(*lookup_context).variantKeyHash(); const std::string Body("hazelcast"); insert(move(lookup_context), getResponseHeaders(), Body); @@ -185,12 +185,12 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. - auto header = cache_->getHeader(variant_hash_key); + auto header = cache_->getHeader(variant_key_hash); Key modified = header->variantKey(); modified.add_custom_fields("custom1"); modified.add_custom_fields("custom2"); header->variantKey(std::move(modified)); - cache_->putHeader(variant_hash_key, *header); + cache_->putHeader(variant_key_hash, *header); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -203,7 +203,7 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); EXPECT_EQ(1, cache_->getTestAccessor().headerMapSize()); - auto modified_header = cache_->getHeader(variant_hash_key); + auto modified_header = cache_->getHeader(variant_key_hash); EXPECT_EQ(*header, *modified_header); } @@ -211,8 +211,8 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { const std::string RequestPath1("/clean/up/on/missing/body"); LookupContextPtr lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - uint64_t variant_hash_key = - static_cast(*lookup_context1).variantHashKey(); + uint64_t variant_key_hash = + static_cast(*lookup_context1).variantKeyHash(); const std::string Body = std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h') + std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z') + @@ -222,13 +222,13 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { lookup_context1 = lookup(RequestPath1); // Response is cached with the following pattern: - // variant_hash_key -> HeaderEntry (in header map) - // variant_hash_key "0" -> Body1 (in body map) - // variant_hash_key "1" -> Body2 (in body map) - // variant_hash_key "2" -> Body3 (in body map) + // variant_key_hash -> HeaderEntry (in header map) + // variant_key_hash "0" -> Body1 (in body map) + // variant_key_hash "1" -> Body2 (in body map) + // variant_key_hash "2" -> Body3 (in body map) EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); - cache_->getTestAccessor().removeBody(cache_->orderedMapKey(variant_hash_key, 1)); // evict Body2. + cache_->getTestAccessor().removeBody(orderedMapKey(variant_key_hash, 1)); // evict Body2. lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); @@ -241,7 +241,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { std::thread t1([&] { // If another thread locks the key, then the current one should not perform // clean up. The lock here will serve the purpose. - EXPECT_TRUE(cache_->tryLock(variant_hash_key)); + EXPECT_TRUE(cache_->tryLock(variant_key_hash)); }); t1.join(); @@ -253,16 +253,16 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { EXPECT_NE(0, cache_->getTestAccessor().bodyMapSize()); // clean up is not performed. - cache_->unlock(variant_hash_key); + cache_->unlock(variant_key_hash); } { // Clean up must be aborted when header versions are mismatched. // This prevents clean up operation for wrong entries. - auto header = cache_->getHeader(variant_hash_key); + auto header = cache_->getHeader(variant_key_hash); int32_t original_version = header->version(); header->version(original_version - 1); - cache_->putHeader(variant_hash_key, *header); + cache_->putHeader(variant_key_hash, *header); lookup_context1->getBody( {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, @@ -271,7 +271,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { EXPECT_NE(0, cache_->getTestAccessor().bodyMapSize()); header->version(original_version); - cache_->putHeader(variant_hash_key, *header); + cache_->putHeader(variant_key_hash, *header); } { @@ -387,18 +387,18 @@ TEST_F(HazelcastDividedCacheTest, FailDuringLock) { // using Local Test Accessor. Changing the order might not cause test to fail but // to uncover some exceptions. const std::string RequestPath("/failed/during/try/lock"); - uint64_t variant_hash_key = - static_cast(*lookup(RequestPath)).variantHashKey(); + uint64_t variant_key_hash = + static_cast(*lookup(RequestPath)).variantKeyHash(); std::thread t1([&] { - // To make this test compatible with RemoteTestCache, the key is locked here + // To make this test compatible with HazelcastRemoteTestCache, the key is locked here // explicitly. This behavior will cause tryLock to return false for further // trials. Hence the lookups below will throw exception for local cache and // return false for remote cache. This has no effect on local test but lack of // this locking causes remote test to fail. Notice that if this locking would not // be performed by a different thread, following insertions in this thread would - // be able to lock the key again and again for RemoteTestCache. - EXPECT_TRUE(cache_->tryLock(variant_hash_key)); + // be able to lock the key again and again for HazelcastRemoteTestCache. + EXPECT_TRUE(cache_->tryLock(variant_key_hash)); }); t1.join(); @@ -409,7 +409,7 @@ TEST_F(HazelcastDividedCacheTest, FailDuringLock) { insert(lookup(RequestPath), getResponseHeaders(), "aborted"); // HazelcastClientOfflineException insert(lookup(RequestPath), getResponseHeaders(), "aborted"); // OperationTimeoutException EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - cache_->unlock(variant_hash_key); + cache_->unlock(variant_key_hash); } TEST_F(HazelcastDividedCacheTest, FailDuringBodyLookupWhenHeaderSucceeds) { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 58f4fd762822c..1939a90e1e97a 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -17,8 +17,8 @@ class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { void SetUp() override { HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(true); // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); + // cache_ = std::make_unique(config); + cache_ = std::make_unique(config); cache_->start(); cache_->getTestAccessor().clearMaps(); } @@ -74,8 +74,8 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - uint64_t variant_hash_key = - static_cast(*lookup_context).variantHashKey(); + uint64_t variant_key_hash = + static_cast(*lookup_context).variantKeyHash(); const std::string Body("hazelcast"); insert(move(lookup_context), getResponseHeaders(), Body); @@ -84,12 +84,12 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { // Manipulate the cache entry directly. Cache is not aware of that. // The cached key will not be the same with the created one by filter. - auto response = cache_->getResponse(variant_hash_key); + auto response = cache_->getResponse(variant_key_hash); Key modified = response->header().variantKey(); modified.add_custom_fields("custom1"); modified.add_custom_fields("custom2"); response->header().variantKey(std::move(modified)); - cache_->getTestAccessor().insertResponse(cache_->mapKey(variant_hash_key), *response); + cache_->getTestAccessor().insertResponse(mapKey(variant_key_hash), *response); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h index 7664e0e19b9b0..000f0a32bbe6a 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h @@ -26,8 +26,8 @@ class TestAccessor { virtual int bodyMapSize() PURE; virtual int responseMapSize() PURE; - virtual void insertResponse(int64_t key, const HazelcastResponseEntry& entry) PURE; - virtual void removeBody(const std::string& key) PURE; + virtual void insertResponse(int64_t map_key, const HazelcastResponseEntry& entry) PURE; + virtual void removeBody(const std::string& map_key) PURE; virtual void failOnLock() PURE; @@ -61,10 +61,10 @@ class RemoteTestAccessor : public TestAccessor, public HazelcastClusterAccessor int responseMapSize() override { return getResponseMap().size(); } - void removeBody(const std::string& key) override { getBodyMap().remove(key); } + void removeBody(const std::string& map_key) override { getBodyMap().remove(map_key); } - void insertResponse(int64_t key, const HazelcastResponseEntry& entry) override { - getResponseMap().put(key, entry); + void insertResponse(int64_t map_key, const HazelcastResponseEntry& entry) override { + getResponseMap().put(map_key, entry); } void failOnLock() override {} // Required for local accessor only. @@ -97,34 +97,34 @@ class LocalTestAccessor : public StorageAccessor, public TestAccessor { int responseMapSize() override { return response_map_.size(); } - void insertResponse(int64_t key, const HazelcastResponseEntry& entry) override { + void insertResponse(int64_t map_key, const HazelcastResponseEntry& entry) override { checkConnection(); - response_map_[key] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); + response_map_[map_key] = HazelcastResponsePtr(new HazelcastResponseEntry(entry)); } - void removeBody(const std::string& key) override { + void removeBody(const std::string& map_key) override { checkConnection(); - removeBodyAsync(key); + removeBodyAsync(map_key); } // StorageAccessor - void putHeader(const int64_t key, const HazelcastHeaderEntry& value) override { + void putHeader(const int64_t map_key, const HazelcastHeaderEntry& value) override { checkConnection(); - header_map_[key] = HazelcastHeaderPtr(new HazelcastHeaderEntry(value)); + header_map_[map_key] = HazelcastHeaderPtr(new HazelcastHeaderEntry(value)); } - void putBody(const std::string& key, const HazelcastBodyEntry& value) override { + void putBody(const std::string& map_key, const HazelcastBodyEntry& value) override { checkConnection(); - body_map_[key] = HazelcastBodyPtr(new HazelcastBodyEntry(value)); + body_map_[map_key] = HazelcastBodyPtr(new HazelcastBodyEntry(value)); } - void putResponse(const int64_t key, const HazelcastResponseEntry& value) override { - insertResponse(key, value); + void putResponse(const int64_t map_key, const HazelcastResponseEntry& value) override { + insertResponse(map_key, value); } - HazelcastHeaderPtr getHeader(const int64_t key) override { + HazelcastHeaderPtr getHeader(const int64_t map_key) override { checkConnection(); - auto result = header_map_.find(key); + auto result = header_map_.find(map_key); if (result != header_map_.end()) { // New objects are created during deserialization. Hence not returning // the original one here. @@ -134,9 +134,9 @@ class LocalTestAccessor : public StorageAccessor, public TestAccessor { } } - HazelcastBodyPtr getBody(const std::string& key) override { + HazelcastBodyPtr getBody(const std::string& map_key) override { checkConnection(); - auto result = body_map_.find(key); + auto result = body_map_.find(map_key); if (result != body_map_.end()) { return HazelcastBodyPtr(new HazelcastBodyEntry(*result->second)); } else { @@ -144,9 +144,9 @@ class LocalTestAccessor : public StorageAccessor, public TestAccessor { } } - HazelcastResponsePtr getResponse(const int64_t key) override { + HazelcastResponsePtr getResponse(const int64_t map_key) override { checkConnection(); - auto result = response_map_.find(key); + auto result = response_map_.find(map_key); if (result != response_map_.end()) { return HazelcastResponsePtr(new HazelcastResponseEntry(*result->second)); } else { @@ -154,46 +154,46 @@ class LocalTestAccessor : public StorageAccessor, public TestAccessor { } } - void removeBodyAsync(const std::string& key) override { + void removeBodyAsync(const std::string& map_key) override { checkConnection(); - body_map_.erase(key); + body_map_.erase(map_key); } - void removeHeader(const int64_t key) override { + void removeHeader(const int64_t map_key) override { checkConnection(); - header_map_.erase(key); + header_map_.erase(map_key); } - bool tryLock(const int64_t key, bool unified) override { + bool tryLock(const int64_t map_key, bool unified) override { checkConnection(fail_on_lock_); if (unified) { bool locked = - std::find(response_locks_.begin(), response_locks_.end(), key) != response_locks_.end(); + std::find(response_locks_.begin(), response_locks_.end(), map_key) != response_locks_.end(); if (locked) { return false; } else { - response_locks_.push_back(key); + response_locks_.push_back(map_key); return true; } } else { bool locked = - std::find(header_locks_.begin(), header_locks_.end(), key) != header_locks_.end(); + std::find(header_locks_.begin(), header_locks_.end(), map_key) != header_locks_.end(); if (locked) { return false; } else { - header_locks_.push_back(key); + header_locks_.push_back(map_key); return true; } } } - void unlock(const int64_t key, bool unified) override { + void unlock(const int64_t map_key, bool unified) override { checkConnection(); if (unified) { - response_locks_.erase(std::remove(response_locks_.begin(), response_locks_.end(), key), + response_locks_.erase(std::remove(response_locks_.begin(), response_locks_.end(), map_key), response_locks_.end()); } else { - header_locks_.erase(std::remove(header_locks_.begin(), header_locks_.end(), key), + header_locks_.erase(std::remove(header_locks_.begin(), header_locks_.end(), map_key), header_locks_.end()); } } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h index 5c054e7c68598..f229040872b0c 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h @@ -28,9 +28,9 @@ class HazelcastHttpTestCache : public HazelcastHttpCache { * * Requires a running Hazelcast instance to be tested. */ -class RemoteTestCache : public HazelcastHttpTestCache { +class HazelcastRemoteTestCache : public HazelcastHttpTestCache { public: - RemoteTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} + HazelcastRemoteTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} void start() override { if (accessor_ && accessor_->isRunning()) { @@ -61,9 +61,9 @@ class RemoteTestCache : public HazelcastHttpTestCache { * Does not require a running Hazelcast instance. Instead, tests the cache * with local storage. This is the way the cache is tested in CI environment. */ -class LocalTestCache : public HazelcastHttpTestCache { +class HazelcastLocalTestCache : public HazelcastHttpTestCache { public: - LocalTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} + HazelcastLocalTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} void start() override { if (!accessor_) { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h index ab5dced125da4..e14d050f20738 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h @@ -63,6 +63,14 @@ class HazelcastHttpCacheTestBase : public testing::Test { protected: HazelcastHttpCacheTestBase() { HazelcastTestUtil::setRequestHeaders(request_headers_); } + int64_t mapKey(const uint64_t key_hash) { + return cache_->mapKey(key_hash); + } + + std::string orderedMapKey(const uint64_t key_hash, const uint64_t order) { + return cache_->orderedMapKey(key_hash, order); + } + // Makes getBody requests until requested range is satisfied. // Returns the body on success; HazelcastTestUtil::abortedBodyResponse() on // abortion by cache. From 570e3629c3229f8c98d27e974e451ca0c2899f21 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Sun, 7 Jun 2020 01:47:00 +0300 Subject: [PATCH 24/33] Use max_body_bytes from CacheConfig. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_cache_plugin.md | 7 +++++ .../cache/hazelcast_http_cache/config.proto | 9 ------- .../hazelcast_http_cache/hazelcast_context.cc | 2 +- .../hazelcast_http_cache.cc | 14 +++++----- .../hazelcast_http_cache.h | 9 ++++--- .../http/cache/hazelcast_http_cache/util.h | 2 +- .../hazelcast_common_cache_test.cc | 17 ++++++------ .../hazelcast_divided_cache_test.cc | 7 ++--- .../hazelcast_unified_cache_test.cc | 7 ++--- .../cache/hazelcast_http_cache/test_caches.h | 9 ++++--- .../cache/hazelcast_http_cache/test_util.h | 27 +++++++++++-------- 11 files changed, 60 insertions(+), 50 deletions(-) diff --git a/source/docs/filters/http/cache/hazelcast_cache_plugin.md b/source/docs/filters/http/cache/hazelcast_cache_plugin.md index 129784f0a3d1b..623c0529dd913 100644 --- a/source/docs/filters/http/cache/hazelcast_cache_plugin.md +++ b/source/docs/filters/http/cache/hazelcast_cache_plugin.md @@ -31,6 +31,13 @@ then three different entries will be created to store the body of this response: size. Also, to keep these partitioned cache entries even, extra operations - not necessarily asynchronous, might be needed (i.e. cleaning up a malformed body sequence, recovery from a mismatch between body and header, etc.). +Maximum body size limit must be configured in CacheConfig. In UNIFIED mode, the maximum allowed body size is 32 KB. +Any value above this will be ignored and 32 KB will be used as the limit. If an insertion for a larger value than +maximum is attempted by the cache filter, only the first 32 KB of the response body will be cached. + +In DIVIDED mode, there is no such an upper limit but keeping (max_body_size / body_partition_size) below 20 is +recommended since each partition causes an extra network call made to the distributed map. + ## Connecting to a Hazelcast cluster **NOTE:** The plugin uses the client with version 3.12.1 and hence it is not yet compatible with Hazelcast 4.x. Hazelcast version 3.12.x is recommended for the server-side. diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto index defa6be369f99..aed388a5d9561 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -69,13 +69,4 @@ message HazelcastHttpCacheConfig { // Body partition size for divided cache. Ignored in unified mode. // At most 32 KB is allowed. 16 KB by default. uint64 body_partition_size = 10; - - // Maximum allowed body size per response. If insertion for a larger - // value than the limit is attempted, the first max_body_size bytes - // of the response will be cached and the remaining will be ignored. - // At most 32 KB is allowed for UNIFIED mode. In DIVIDED mode, there - // is no such an upper limit but keeping (max_body_size / body_partition_size) - // below 20 is recommended since each partition causes an extra - // network call made to the distributed map. - uint64 max_body_size = 11; } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index c3b82a93aa836..b78d2db307a07 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -87,7 +87,7 @@ void HazelcastLookupContextBase::arrangeVariantHeaders( HazelcastInsertContextBase::HazelcastInsertContextBase(LookupContext& lookup_context, HazelcastHttpCache& cache) - : hz_cache_(cache), max_body_size_(cache.maxBodySize()), + : hz_cache_(cache), max_body_size_(cache.maxBodyBytes()), variant_key_hash_(static_cast(lookup_context).variantKeyHash()), variant_key_(static_cast(lookup_context).variantKey()), abort_insertion_(static_cast(lookup_context).isAborted()) {} diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index c502a5b108145..f8727e74b1939 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -13,11 +13,11 @@ using hazelcast::client::ClientConfig; using hazelcast::client::exception::HazelcastClientOfflineException; using hazelcast::client::serialization::DataSerializableFactory; -HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig config) - : unified_(config.unified()), - body_partition_size_(ConfigUtil::validPartitionSize(config.body_partition_size())), - max_body_size_(ConfigUtil::validMaxBodySize(config.max_body_size(), config.unified())), - cache_config_(config) {} +HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : + unified_(typed_config.unified()), + body_partition_size_(ConfigUtil::validPartitionSize(typed_config.body_partition_size())), + max_body_size_(ConfigUtil::validMaxBodySize(cache_config.max_body_bytes(), typed_config.unified())), + cache_config_(std::move(typed_config)) {} void HazelcastHttpCache::onMissingBody(uint64_t key_hash, int32_t version, uint64_t body_size) { try { @@ -154,7 +154,7 @@ HttpCache& HazelcastHttpCacheFactory::getCache( if (!cache_) { HazelcastHttpCacheConfig hz_cache_config; MessageUtil::unpackTo(config.typed_config(), hz_cache_config); - cache_ = std::make_unique(hz_cache_config); + cache_ = std::make_unique(std::move(hz_cache_config), config); cache_->start(); } return *cache_; @@ -165,7 +165,7 @@ HazelcastHttpCachePtr HazelcastHttpCacheFactory::getOfflineCache( if (!cache_) { HazelcastHttpCacheConfig hz_cache_config; MessageUtil::unpackTo(config.typed_config(), hz_cache_config); - cache_ = std::make_unique(hz_cache_config); + cache_ = std::make_unique(std::move(hz_cache_config), config); } return std::move(cache_); } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 1d594988b4abe..93a7ab4b74318 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -42,7 +42,8 @@ using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; class HazelcastHttpCache : public HttpCache, public Logger::Loggable { public: - HazelcastHttpCache(HazelcastHttpCacheConfig config); + HazelcastHttpCache(HazelcastHttpCacheConfig&& typed_config, + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config); /// Divided mode @@ -167,7 +168,7 @@ class HazelcastHttpCache : public HttpCache, * than this limit, the first max_body_size_ bytes of the response * will be cached only. */ - uint64_t maxBodySize() const { return max_body_size_; } + uint32_t maxBodyBytes() const { return max_body_bytes_; } /** * Makes the cache ready to serve. Storage accessor connection must be established @@ -236,8 +237,8 @@ class HazelcastHttpCache : public HttpCache, /** Partition size in bytes for a single body entry */ const uint64_t body_partition_size_; - /** Allowed max body size in bytes for a response */ - const uint64_t max_body_size_; + /** Allowed max body size for a response */ + const uint32_t max_body_bytes_; /** typed config from CacheConfig */ HazelcastHttpCacheConfig cache_config_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h index e927154095ce8..6d38e9dacfd0f 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -19,7 +19,7 @@ class ConfigUtil { : config_value; } - static uint64_t validMaxBodySize(const uint64_t config_value, const bool unified) { + static uint32_t validMaxBodySize(const uint32_t config_value, const bool unified) { if (unified) { // Apply size limitation for single entry (unified response) on the map. return config_value == 0 || (config_value > MAX_ALLOWED_UNIFIED_BODY_SIZE) diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 4f39beed6e1c4..ddfb21cdbfc72 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -17,10 +17,11 @@ class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, public testing::WithParamInterface { protected: void SetUp() override { - HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(GetParam()); + HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(GetParam()); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); + // cache_ = std::make_unique(std::move(typed_config), cache_config); + cache_ = std::make_unique(std::move(typed_config), cache_config); cache_->start(); cache_->getTestAccessor().clearMaps(); } @@ -217,11 +218,11 @@ TEST(Registration, GetFactory) { "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); ASSERT_NE(factory, nullptr); envoy::extensions::filters::http::cache::v3alpha::CacheConfig config; - HazelcastHttpCacheConfig hz_cache_config = HazelcastTestUtil::getTestConfig(true); - hz_cache_config.set_group_name("do-not-connect-any-cluster"); - hz_cache_config.set_connection_attempt_limit(1); - hz_cache_config.set_connection_attempt_period(1); // give up immediately. - config.mutable_typed_config()->PackFrom(hz_cache_config); + HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(true); + typed_config.set_group_name("do-not-connect-any-cluster"); + typed_config.set_connection_attempt_limit(1); + typed_config.set_connection_attempt_period(1); // give up immediately. + config.mutable_typed_config()->PackFrom(typed_config); { // getOfflineCache() call is for testing. It creates a HazelcastHttpCache but does diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 33aa2a55bcfb0..dce8a6b41edc0 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -16,10 +16,11 @@ namespace HazelcastHttpCache { class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { protected: void SetUp() override { - HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(false); + HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(false); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); + // cache_ = std::make_unique(std::move(typed_config), cache_config); + cache_ = std::make_unique(std::move(typed_config), cache_config); cache_->start(); cache_->getTestAccessor().clearMaps(); } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 1939a90e1e97a..952d5e57e9845 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -15,10 +15,11 @@ namespace HazelcastHttpCache { */ class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { void SetUp() override { - HazelcastHttpCacheConfig config = HazelcastTestUtil::getTestConfig(true); + HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(true); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(config); - cache_ = std::make_unique(config); + // cache_ = std::make_unique(std::move(typed_config), cache_config); + cache_ = std::make_unique(std::move(typed_config), cache_config); cache_->start(); cache_->getTestAccessor().clearMaps(); } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h index f229040872b0c..166a55b10d91b 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h @@ -18,7 +18,8 @@ namespace HazelcastHttpCache { */ class HazelcastHttpTestCache : public HazelcastHttpCache { public: - HazelcastHttpTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpCache(std::move(config)) {} + HazelcastHttpTestCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : + HazelcastHttpCache(std::move(typed_config), cache_config) {} TestAccessor& getTestAccessor() { return dynamic_cast(*accessor_); } }; @@ -30,7 +31,8 @@ class HazelcastHttpTestCache : public HazelcastHttpCache { */ class HazelcastRemoteTestCache : public HazelcastHttpTestCache { public: - HazelcastRemoteTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} + HazelcastRemoteTestCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : + HazelcastHttpTestCache(std::move(typed_config), cache_config) {} void start() override { if (accessor_ && accessor_->isRunning()) { @@ -63,7 +65,8 @@ class HazelcastRemoteTestCache : public HazelcastHttpTestCache { */ class HazelcastLocalTestCache : public HazelcastHttpTestCache { public: - HazelcastLocalTestCache(HazelcastHttpCacheConfig config) : HazelcastHttpTestCache(std::move(config)) {} + HazelcastLocalTestCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : + HazelcastHttpTestCache(std::move(typed_config), cache_config) {} void start() override { if (!accessor_) { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h index e14d050f20738..0a21940f6fba6 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h @@ -27,21 +27,26 @@ class HazelcastTestUtil { return response; } - static HazelcastHttpCacheConfig getTestConfig(bool unified) { - HazelcastHttpCacheConfig hc; - hc.set_group_name("dev"); - hc.set_group_password("dev-pass"); - HazelcastHttpCacheConfig::MemberAddress* memberAddress = hc.add_addresses(); + static envoy::extensions::filters::http::cache::v3alpha::CacheConfig getTestCacheConfig() { + envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config; + cache_config.set_max_body_bytes(TEST_MAX_BODY_SIZE); + return cache_config; + } + + static HazelcastHttpCacheConfig getTestTypedConfig(bool unified) { + HazelcastHttpCacheConfig typed_config; + typed_config.set_group_name("dev"); + typed_config.set_group_password("dev-pass"); + HazelcastHttpCacheConfig::MemberAddress* memberAddress = typed_config.add_addresses(); memberAddress->set_ip("127.0.0.1"); memberAddress->set_port(5701); - hc.set_invocation_timeout(1); - hc.set_body_partition_size(TEST_PARTITION_SIZE); + typed_config.set_invocation_timeout(1); + typed_config.set_body_partition_size(TEST_PARTITION_SIZE); // During parallel tests, if caches do not have different prefixes, the entries // and hence the results will be different than the expected. - hc.set_app_prefix(randomGenerator().uuid()); - hc.set_unified(unified); - hc.set_max_body_size(TEST_MAX_BODY_SIZE); - return hc; + typed_config.set_app_prefix(randomGenerator().uuid()); + typed_config.set_unified(unified); + return typed_config; } static void setRequestHeaders(Http::TestRequestHeaderMapImpl& headers) { From 66a1f013070043415de788f6abe73887e52561c3 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Wed, 10 Jun 2020 10:47:04 +0300 Subject: [PATCH 25/33] Use core.SocketAddress for member address. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_http_cache/BUILD | 1 + .../cache/hazelcast_http_cache/config.proto | 22 ++++++++----------- .../hazelcast_http_cache.cc | 4 ++-- .../http/cache/hazelcast_http_cache/util.h | 2 +- .../hazelcast_cache_config_test.cc | 14 ++++++------ .../cache/hazelcast_http_cache/test_util.h | 6 ++--- 6 files changed, 23 insertions(+), 26 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD index a7e998d39aa23..a4519301b4569 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -41,4 +41,5 @@ envoy_cc_extension( envoy_proto_library( name = "config", srcs = ["config.proto"], + deps = ["@envoy_api//envoy/config/core/v3:pkg"], ) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto index aed388a5d9561..a8a92bc5d0a20 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/config.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.source.extensions.filters.http.cache; +import "envoy/config/core/v3/address.proto"; + // [#protodoc-title: HazelcastHttpCache CacheFilter storage plugin] // CacheFilter plugin backed by Hazelcast In Memory Data Grid. // [#extension: envoy.extensions.http.cache] @@ -9,16 +11,9 @@ package envoy.source.extensions.filters.http.cache; // Hazelcast Http Cache configuration message HazelcastHttpCacheConfig { - // Address for Hazelcast cluster member to be connected. - message MemberAddress { - string ip = 1; - int32 port = 2; - } - - // Group name of Hazelcast cluster to be connected. Not only the address of a member - // but its group name and password must match. + // Group name of the Hazelcast cluster to be connected. string group_name = 1; - // Group password of Hazelcast cluster to be connected. + // Group password of the Hazelcast cluster to be connected. string group_password = 2; // The timeout value in milliseconds for Hazelcast members to accept this client's @@ -43,10 +38,11 @@ message HazelcastHttpCacheConfig { // 8 by default and 0 is not allowed. uint32 invocation_timeout = 6; - // Only one member address is enough to connect to the cluster but - // providing more than one is recommended. By default, 127.0.0.1:5701 will - // be tried. - repeated MemberAddress addresses = 7; + // Address and port value of the cluster members. Other fields such as protocol, resolver_name, etc. + // are not required. Only one Hazelcast member address is enough to connect to the cluster but + // providing more than one is recommended. If no address is provided, 127.0.0.1:5701 will be tried + // by default. + repeated envoy.config.core.v3.SocketAddress addresses = 7; // Application specific name for the cache. Different deployments should // use the same prefix and connect to the same Hazelcast cluster if they want diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index f8727e74b1939..9e669af05d735 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -16,7 +16,7 @@ using hazelcast::client::serialization::DataSerializableFactory; HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : unified_(typed_config.unified()), body_partition_size_(ConfigUtil::validPartitionSize(typed_config.body_partition_size())), - max_body_size_(ConfigUtil::validMaxBodySize(cache_config.max_body_bytes(), typed_config.unified())), + max_body_bytes_(ConfigUtil::validMaxBodySize(cache_config.max_body_bytes(), typed_config.unified())), cache_config_(std::move(typed_config)) {} void HazelcastHttpCache::onMissingBody(uint64_t key_hash, int32_t version, uint64_t body_size) { @@ -79,7 +79,7 @@ void HazelcastHttpCache::start() { ENVOY_LOG(info, "HazelcastHttpCache has been started with profile: {}. Max body size: {}.", unified_ ? "UNIFIED" : "DIVIDED, partition size: " + std::to_string(body_partition_size_), - max_body_size_); + max_body_bytes_); HazelcastClusterAccessor& cluster_accessor = static_cast(*accessor_); ENVOY_LOG(info, "Cache statistics can be observed on Hazelcast Management Center" diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h index 6d38e9dacfd0f..9be859b8adc39 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -41,7 +41,7 @@ class ConfigUtil { config.getGroupConfig().setPassword(cache_config.group_password()); for (auto& address : cache_config.addresses()) { config.getNetworkConfig().addAddress( - hazelcast::client::Address(address.ip(), address.port())); + hazelcast::client::Address(address.address(), address.port_value())); } config.getNetworkConfig().setConnectionTimeout(cache_config.connection_timeout() == 0 ? DEFAULT_CONNECTION_TIMEOUT_MS diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc index 80206a67a3624..49351d9701d3b 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc @@ -62,15 +62,15 @@ TEST_F(ConfigUtilsTest, ValidMaxBodySizeTest) { TEST_F(ConfigUtilsTest, ClientConfigTest) { const std::string group_name = "group_foo"; const std::string group_pass = "foo_pass"; - const std::string random_ip = "192.168.10.3"; - constexpr int random_port = 5703; + const std::string member_address = "192.168.10.3"; // arbitrary address + constexpr int member_port = 5703; // arbitrary port HazelcastHttpCacheConfig default_cache_config; default_cache_config.set_group_name(group_name); default_cache_config.set_group_password(group_pass); - HazelcastHttpCacheConfig::MemberAddress* memberAddress = default_cache_config.add_addresses(); - memberAddress->set_ip(random_ip); - memberAddress->set_port(random_port); + ::envoy::config::core::v3::SocketAddress* address = default_cache_config.add_addresses(); + address->set_address(member_address); + address->set_port_value(member_port); hazelcast::client::ClientConfig config = ConfigUtil::getClientConfig(default_cache_config); @@ -86,8 +86,8 @@ TEST_F(ConfigUtilsTest, ClientConfigTest) { EXPECT_STREQ(group_pass.c_str(), config.getGroupConfig().getPassword().c_str()); std::vector addresses = config.getNetworkConfig().getAddresses(); EXPECT_EQ(1, addresses.size()); - EXPECT_STREQ(random_ip.c_str(), addresses.at(0).getHost().c_str()); - EXPECT_EQ(random_port, addresses.at(0).getPort()); + EXPECT_STREQ(member_address.c_str(), addresses.at(0).getHost().c_str()); + EXPECT_EQ(member_port, addresses.at(0).getPort()); } TEST_F(ConfigUtilsTest, WarnLimitTest) { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h index 0a21940f6fba6..eaef8b4231157 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h @@ -37,9 +37,9 @@ class HazelcastTestUtil { HazelcastHttpCacheConfig typed_config; typed_config.set_group_name("dev"); typed_config.set_group_password("dev-pass"); - HazelcastHttpCacheConfig::MemberAddress* memberAddress = typed_config.add_addresses(); - memberAddress->set_ip("127.0.0.1"); - memberAddress->set_port(5701); + envoy::config::core::v3::SocketAddress* member_address = typed_config.add_addresses(); + member_address->set_address("127.0.0.1"); + member_address->set_port_value(5701); typed_config.set_invocation_timeout(1); typed_config.set_body_partition_size(TEST_PARTITION_SIZE); // During parallel tests, if caches do not have different prefixes, the entries From 0acc4d14bece0740285bb7e5ebcb980325182955 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Thu, 11 Jun 2020 11:14:22 +0300 Subject: [PATCH 26/33] Fix format. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_cache_plugin.md | 2 +- .../hazelcast_http_cache/hazelcast_context.cc | 10 ++++------ .../hazelcast_http_cache.cc | 9 ++++++--- .../hazelcast_http_cache.h | 7 +++++-- .../hazelcast_storage_accessor.cc | 10 +++++++--- .../hazelcast_cache_config_test.cc | 2 +- .../hazelcast_common_cache_test.cc | 4 ++-- .../hazelcast_divided_cache_test.cc | 13 +++++++------ .../hazelcast_unified_cache_test.cc | 3 ++- .../hazelcast_http_cache/test_accessors.h | 4 ++-- .../cache/hazelcast_http_cache/test_caches.h | 18 ++++++++++++------ .../cache/hazelcast_http_cache/test_util.h | 4 +--- 12 files changed, 50 insertions(+), 36 deletions(-) diff --git a/source/docs/filters/http/cache/hazelcast_cache_plugin.md b/source/docs/filters/http/cache/hazelcast_cache_plugin.md index 623c0529dd913..61d5633ca80de 100644 --- a/source/docs/filters/http/cache/hazelcast_cache_plugin.md +++ b/source/docs/filters/http/cache/hazelcast_cache_plugin.md @@ -4,7 +4,7 @@ Work in Progress--Cache filter has not implemented features. The corresponding o Hazelcast Http Cache provides a pluggable storage implementation backed by Hazelcast In Memory Data Grid for the Http cache filter. Using Hazelcast C++ client, the plugin does not store any Http response locally but in a distributed map provided by Hazelcast cluster. After having a Hazelcast cluster up and running, passing the network address of a -cluster member to the cache plugin will be enough for client to connect to the cluster. +cluster member to the cache plugin via configuration will be enough for client to connect to the cluster. ## Offered cache modes The plugin comes with two modes: diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc index b78d2db307a07..d4b23d18af7f5 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.cc @@ -124,9 +124,8 @@ void UnifiedLookupContext::getHeaders(LookupHeadersCallback&& cb) { // is performed here. If a different response is found with the same // hash (probably on hash collisions), the new response is denied to // be cached and the old one remains. - handleLookupFailure("Mismatched keys found for key hash: " + - std::to_string(variant_key_hash_), - cb, false); + handleLookupFailure( + "Mismatched keys found for key hash: " + std::to_string(variant_key_hash_), cb, false); return; } cb(lookup_request_.makeLookupResult(std::move(response_->header().headerMap()), @@ -246,9 +245,8 @@ void DividedLookupContext::getHeaders(LookupHeadersCallback&& cb) { ENVOY_LOG(debug, "Found divided response: [key: {}u, version: {}, body size: {}]", variant_key_hash_, header_entry->version(), header_entry->bodySize()); if (!MessageDifferencer::Equals(header_entry->variantKey(), variantKey())) { - handleLookupFailure("Mismatched keys found for key hash: " + - std::to_string(variant_key_hash_), - cb, false); + handleLookupFailure( + "Mismatched keys found for key hash: " + std::to_string(variant_key_hash_), cb, false); return; } this->total_body_size_ = header_entry->bodySize(); diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index 9e669af05d735..9a44774dd1afc 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -13,10 +13,13 @@ using hazelcast::client::ClientConfig; using hazelcast::client::exception::HazelcastClientOfflineException; using hazelcast::client::serialization::DataSerializableFactory; -HazelcastHttpCache::HazelcastHttpCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : - unified_(typed_config.unified()), +HazelcastHttpCache::HazelcastHttpCache( + HazelcastHttpCacheConfig&& typed_config, + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) + : unified_(typed_config.unified()), body_partition_size_(ConfigUtil::validPartitionSize(typed_config.body_partition_size())), - max_body_bytes_(ConfigUtil::validMaxBodySize(cache_config.max_body_bytes(), typed_config.unified())), + max_body_bytes_( + ConfigUtil::validMaxBodySize(cache_config.max_body_bytes(), typed_config.unified())), cache_config_(std::move(typed_config)) {} void HazelcastHttpCache::onMissingBody(uint64_t key_hash, int32_t version, uint64_t body_size) { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 93a7ab4b74318..7b3676a0fdbc7 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -42,7 +42,8 @@ using envoy::source::extensions::filters::http::cache::HazelcastHttpCacheConfig; class HazelcastHttpCache : public HttpCache, public Logger::Loggable { public: - HazelcastHttpCache(HazelcastHttpCacheConfig&& typed_config, + HazelcastHttpCache( + HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config); /// Divided mode @@ -77,7 +78,9 @@ class HazelcastHttpCache : public HttpCache, * @param key_hash Hash of the filter's cache key * @return HazelcastHeaderPtr to cached entry if found, nullptr otherwise */ - HazelcastHeaderPtr getHeader(const uint64_t key_hash) { return accessor_->getHeader(mapKey(key_hash)); } + HazelcastHeaderPtr getHeader(const uint64_t key_hash) { + return accessor_->getHeader(mapKey(key_hash)); + } /** * Performs a lookup to body cache for the given key hash and order pair. diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc index 1bed74c68c011..a200d8c624fff 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc @@ -12,7 +12,8 @@ void HazelcastClusterAccessor::putHeader(const int64_t map_key, const HazelcastH getHeaderMap().set(map_key, value); } -void HazelcastClusterAccessor::putBody(const std::string& map_key, const HazelcastBodyEntry& value) { +void HazelcastClusterAccessor::putBody(const std::string& map_key, + const HazelcastBodyEntry& value) { getBodyMap().set(map_key, value); } @@ -28,9 +29,12 @@ void HazelcastClusterAccessor::removeBodyAsync(const std::string& map_key) { getBodyMap().removeAsync(map_key); } -void HazelcastClusterAccessor::removeHeader(const int64_t map_key) { getHeaderMap().deleteEntry(map_key); } +void HazelcastClusterAccessor::removeHeader(const int64_t map_key) { + getHeaderMap().deleteEntry(map_key); +} -void HazelcastClusterAccessor::putResponse(const int64_t map_key, const HazelcastResponseEntry& value) { +void HazelcastClusterAccessor::putResponse(const int64_t map_key, + const HazelcastResponseEntry& value) { getResponseMap().set(map_key, value); } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc index 49351d9701d3b..0fa750e28eac8 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_config_test.cc @@ -63,7 +63,7 @@ TEST_F(ConfigUtilsTest, ClientConfigTest) { const std::string group_name = "group_foo"; const std::string group_pass = "foo_pass"; const std::string member_address = "192.168.10.3"; // arbitrary address - constexpr int member_port = 5703; // arbitrary port + constexpr int member_port = 5703; // arbitrary port HazelcastHttpCacheConfig default_cache_config; default_cache_config.set_group_name(group_name); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index ddfb21cdbfc72..5533e4fab760f 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -18,7 +18,8 @@ class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, protected: void SetUp() override { HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(GetParam()); - envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = + HazelcastTestUtil::getTestCacheConfig(); // To test the cache with a real Hazelcast instance, use remote test cache. // cache_ = std::make_unique(std::move(typed_config), cache_config); cache_ = std::make_unique(std::move(typed_config), cache_config); @@ -233,7 +234,6 @@ TEST(Registration, GetFactory) { EXPECT_EQ(cache->cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); EXPECT_THROW_WITH_MESSAGE(cache->start(), EnvoyException, "Hazelcast Client could not connect to any cluster."); - cache.reset(); } EXPECT_THROW_WITH_MESSAGE(factory->getCache(config), EnvoyException, "Hazelcast Client could not connect to any cluster."); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index dce8a6b41edc0..6ca5063a5f3f6 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -17,7 +17,8 @@ class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { protected: void SetUp() override { HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(false); - envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = + HazelcastTestUtil::getTestCacheConfig(); // To test the cache with a real Hazelcast instance, use remote test cache. // cache_ = std::make_unique(std::move(typed_config), cache_config); cache_ = std::make_unique(std::move(typed_config), cache_config); @@ -77,14 +78,14 @@ TEST_F(HazelcastDividedCacheTest, CleanBodyOnHeaderEviction) { EXPECT_EQ(1, cache_->getTestAccessor().headerMapSize()); EXPECT_EQ(BodyCount, cache_->getTestAccessor().bodyMapSize()); - auto keyObject = std::make_unique(mapKey(variant_key_hash)); - auto valueObject = cache_->getHeader(variant_key_hash); - EXPECT_NE(nullptr, valueObject); + auto key_object = std::make_unique(mapKey(variant_key_hash)); + auto value_object = cache_->getHeader(variant_key_hash); + EXPECT_NE(nullptr, value_object); Member m; MockEntryEvictedEvent mock_event(m); - EXPECT_CALL(mock_event, getOldValueObject()).WillRepeatedly(testing::Return(valueObject.get())); - EXPECT_CALL(mock_event, getKeyObject()).WillRepeatedly(testing::Return(keyObject.get())); + EXPECT_CALL(mock_event, getOldValueObject()).WillRepeatedly(testing::Return(value_object.get())); + EXPECT_CALL(mock_event, getKeyObject()).WillRepeatedly(testing::Return(key_object.get())); EXPECT_EQ(BodyCount, cache_->getTestAccessor().bodyMapSize()); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 952d5e57e9845..77470f56f4c05 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -16,7 +16,8 @@ namespace HazelcastHttpCache { class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { void SetUp() override { HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(true); - envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); + envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = + HazelcastTestUtil::getTestCacheConfig(); // To test the cache with a real Hazelcast instance, use remote test cache. // cache_ = std::make_unique(std::move(typed_config), cache_config); cache_ = std::make_unique(std::move(typed_config), cache_config); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h index 000f0a32bbe6a..a62477a7e4b8e 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h @@ -167,8 +167,8 @@ class LocalTestAccessor : public StorageAccessor, public TestAccessor { bool tryLock(const int64_t map_key, bool unified) override { checkConnection(fail_on_lock_); if (unified) { - bool locked = - std::find(response_locks_.begin(), response_locks_.end(), map_key) != response_locks_.end(); + bool locked = std::find(response_locks_.begin(), response_locks_.end(), map_key) != + response_locks_.end(); if (locked) { return false; } else { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h index 166a55b10d91b..87db42d85b94e 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h @@ -18,8 +18,10 @@ namespace HazelcastHttpCache { */ class HazelcastHttpTestCache : public HazelcastHttpCache { public: - HazelcastHttpTestCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : - HazelcastHttpCache(std::move(typed_config), cache_config) {} + HazelcastHttpTestCache( + HazelcastHttpCacheConfig&& typed_config, + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) + : HazelcastHttpCache(std::move(typed_config), cache_config) {} TestAccessor& getTestAccessor() { return dynamic_cast(*accessor_); } }; @@ -31,8 +33,10 @@ class HazelcastHttpTestCache : public HazelcastHttpCache { */ class HazelcastRemoteTestCache : public HazelcastHttpTestCache { public: - HazelcastRemoteTestCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : - HazelcastHttpTestCache(std::move(typed_config), cache_config) {} + HazelcastRemoteTestCache( + HazelcastHttpCacheConfig&& typed_config, + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) + : HazelcastHttpTestCache(std::move(typed_config), cache_config) {} void start() override { if (accessor_ && accessor_->isRunning()) { @@ -65,8 +69,10 @@ class HazelcastRemoteTestCache : public HazelcastHttpTestCache { */ class HazelcastLocalTestCache : public HazelcastHttpTestCache { public: - HazelcastLocalTestCache(HazelcastHttpCacheConfig&& typed_config, const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) : - HazelcastHttpTestCache(std::move(typed_config), cache_config) {} + HazelcastLocalTestCache( + HazelcastHttpCacheConfig&& typed_config, + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) + : HazelcastHttpTestCache(std::move(typed_config), cache_config) {} void start() override { if (!accessor_) { diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h index eaef8b4231157..0cbe62f1a66a0 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h @@ -68,9 +68,7 @@ class HazelcastHttpCacheTestBase : public testing::Test { protected: HazelcastHttpCacheTestBase() { HazelcastTestUtil::setRequestHeaders(request_headers_); } - int64_t mapKey(const uint64_t key_hash) { - return cache_->mapKey(key_hash); - } + int64_t mapKey(const uint64_t key_hash) { return cache_->mapKey(key_hash); } std::string orderedMapKey(const uint64_t key_hash, const uint64_t order) { return cache_->orderedMapKey(key_hash, order); From d3b0cf5823b2891844090e5ad624a012d0240949 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Thu, 11 Jun 2020 11:47:41 +0300 Subject: [PATCH 27/33] Fix Windows lib name. Signed-off-by: Enes Ozcan --- bazel/foreign_cc/BUILD | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bazel/foreign_cc/BUILD b/bazel/foreign_cc/BUILD index b5541ce1537f8..81cb6c78b171a 100644 --- a/bazel/foreign_cc/BUILD +++ b/bazel/foreign_cc/BUILD @@ -179,12 +179,12 @@ envoy_cmake_external( cache_entries = { "HZ_LIB_TYPE": "STATIC", "CMAKE_BUILD_TYPE": "RELEASE", - "CMAKE_CXX_FLAGS": "-Wno-deprecated", }, lib_source = "@com_github_hazelcast_cpp_client//:all", static_libraries = select({ - "//bazel:windows_x86_64": ["libHazelcastClient3.12.1_64.lib"], + "//bazel:windows_x86_64": ["HazelcastClient3.12.1_64.lib"], "//conditions:default": ["libHazelcastClient3.12.1_64.a"], + }), ) From eb509429f73ca48cfa6052640ae2647fc7f08ad0 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Thu, 11 Jun 2020 15:54:26 +0300 Subject: [PATCH 28/33] Fix format. Signed-off-by: Enes Ozcan --- bazel/foreign_cc/BUILD | 1 - .../extensions/filters/http/cache/hazelcast_http_cache/BUILD | 4 ++-- test/extensions/filters/http/cache/hazelcast_http_cache/BUILD | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bazel/foreign_cc/BUILD b/bazel/foreign_cc/BUILD index 22921d631129e..84b7d62e46636 100644 --- a/bazel/foreign_cc/BUILD +++ b/bazel/foreign_cc/BUILD @@ -199,7 +199,6 @@ envoy_cmake_external( static_libraries = select({ "//bazel:windows_x86_64": ["HazelcastClient3.12.1_64.lib"], "//conditions:default": ["libHazelcastClient3.12.1_64.a"], - }), ) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD index a4519301b4569..8552fa77d814d 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -1,5 +1,3 @@ -licenses(["notice"]) # Apache 2 - load( "//bazel:envoy_build_system.bzl", "envoy_cc_extension", @@ -7,6 +5,8 @@ load( "envoy_proto_library", ) +licenses(["notice"]) # Apache 2 + envoy_package() envoy_cc_extension( diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 4798a1f828a3f..26abff2824ac7 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -1,5 +1,3 @@ -licenses(["notice"]) # Apache 2 - load("//bazel:envoy_build_system.bzl", "envoy_package") load( "//test/extensions:extensions_build_system.bzl", @@ -7,6 +5,8 @@ load( "envoy_extension_cc_test_library", ) +licenses(["notice"]) # Apache 2 + envoy_package() envoy_extension_cc_test( From 7413c5488b61841ea9e812a947179ced3b3650a0 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Sat, 13 Jun 2020 01:46:37 +0300 Subject: [PATCH 29/33] Remove accessor cast and move logs to accessor. Signed-off-by: Enes Ozcan --- .../hazelcast_http_cache.cc | 10 +------- .../hazelcast_http_cache.h | 2 ++ .../hazelcast_storage_accessor.cc | 25 +++++++++++-------- .../hazelcast_storage_accessor.h | 14 +++++------ .../hazelcast_unified_cache_test.cc | 2 -- .../hazelcast_http_cache/test_accessors.h | 6 +++-- 6 files changed, 28 insertions(+), 31 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index 9a44774dd1afc..eb2b603c273b4 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -79,15 +79,7 @@ void HazelcastHttpCache::start() { accessor_.reset(); throw EnvoyException("Hazelcast Client could not connect to any cluster."); } - ENVOY_LOG(info, "HazelcastHttpCache has been started with profile: {}. Max body size: {}.", - unified_ ? "UNIFIED" - : "DIVIDED, partition size: " + std::to_string(body_partition_size_), - max_body_bytes_); - HazelcastClusterAccessor& cluster_accessor = static_cast(*accessor_); - ENVOY_LOG(info, - "Cache statistics can be observed on Hazelcast Management Center" - " from the map named {}.", - unified_ ? cluster_accessor.responseMapName() : cluster_accessor.headerMapName()); + ENVOY_LOG(info, accessor_->startInfo()); } void HazelcastHttpCache::shutdown(bool destroy) { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 7b3676a0fdbc7..3da4f1b322fe5 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -173,6 +173,8 @@ class HazelcastHttpCache : public HttpCache, */ uint32_t maxBodyBytes() const { return max_body_bytes_; } + bool unified() const { return unified_; } + /** * Makes the cache ready to serve. Storage accessor connection must be established * via StorageAccessor::connect() when the cache is started. diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc index a200d8c624fff..9087b51f89408 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc @@ -64,11 +64,11 @@ void HazelcastClusterAccessor::unlock(const int64_t map_key, bool unified) { } } -bool HazelcastClusterAccessor::isRunning() { +bool HazelcastClusterAccessor::isRunning() const { return hazelcast_client_ ? hazelcast_client_->getLifecycleService().isRunning() : false; } -std::string HazelcastClusterAccessor::clusterName() { +std::string HazelcastClusterAccessor::clusterName() const { return hazelcast_client_ ? hazelcast_client_->getClientConfig().getGroupConfig().getName() : ""; } @@ -78,10 +78,6 @@ void HazelcastClusterAccessor::disconnect() { } } -const std::string& HazelcastClusterAccessor::headerMapName() { return header_map_name_; } - -const std::string& HazelcastClusterAccessor::responseMapName() { return response_map_name_; } - HazelcastClusterAccessor::HazelcastClusterAccessor(HazelcastHttpCache& cache, ClientConfig&& client_config, const std::string& app_prefix, @@ -102,12 +98,19 @@ void HazelcastClusterAccessor::connect() { getHeaderMap().addEntryListener(*listener_, true); } +std::string HazelcastClusterAccessor::startInfo() const { + return absl::StrFormat("HazelcastHttpCache is created with profile: %s. Max body size: %d.\n" + "Cache statistics can be observed on Hazelcast Management Center " + "from the map named %s.", + cache_.unified() ? + "UNIFIED" : + "DIVIDED, partition size: " + std::to_string(partition_size_), + cache_.maxBodyBytes(), + cache_.unified() ? response_map_name_ : header_map_name_); +} + std::string HazelcastClusterAccessor::constructMapName(const std::string& postfix, bool unified) { - std::string name(app_prefix_); - if (!unified) { - name.append(":").append(std::to_string(partition_size_)); - } - return name.append("-").append(postfix); + return absl::StrFormat("%s-%d-%s", app_prefix_, unified ? cache_.maxBodyBytes() : partition_size_, postfix); } void HeaderMapEntryListener::entryEvicted(const EntryEvent& event) { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h index 1af7bf2d5ad17..0fcd17cad70d1 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h @@ -2,6 +2,7 @@ #include "envoy/common/exception.h" +#include "absl/strings/str_format.h" #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" #include "hazelcast/client/HazelcastClient.h" @@ -37,8 +38,9 @@ class StorageAccessor { virtual bool tryLock(const int64_t map_key, bool unified) PURE; virtual void unlock(const int64_t map_key, bool unified) PURE; - virtual bool isRunning() PURE; - virtual std::string clusterName() PURE; + virtual bool isRunning() const PURE; + virtual std::string clusterName() const PURE; + virtual std::string startInfo() const PURE; virtual void connect() PURE; virtual void disconnect() PURE; @@ -73,15 +75,13 @@ class HazelcastClusterAccessor : public StorageAccessor { bool tryLock(const int64_t map_key, bool unified) override; void unlock(const int64_t map_key, bool unified) override; - bool isRunning() override; - std::string clusterName() override; + bool isRunning() const override; + std::string clusterName() const override; + std::string startInfo() const override; void connect() override; void disconnect() override; - const std::string& headerMapName(); - const std::string& responseMapName(); - ~HazelcastClusterAccessor() override = default; private: diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 77470f56f4c05..589f51f9bda9b 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -158,8 +158,6 @@ TEST_F(HazelcastUnifiedCacheTest, CoverRemoteOperations) { HazelcastClusterAccessor accessor(*cache_, ClientConfig(), "coverage", 10); EXPECT_FALSE(accessor.isRunning()); EXPECT_STREQ("", accessor.clusterName().c_str()); - EXPECT_STREQ("coverage:10-div", accessor.headerMapName().c_str()); - EXPECT_STREQ("coverage-uni", accessor.responseMapName().c_str()); EXPECT_THROW(accessor.putHeader(1, HazelcastHeaderEntry()), EnvoyException); EXPECT_THROW(accessor.putBody("1", HazelcastBodyEntry()), EnvoyException); EXPECT_THROW(accessor.putResponse(1, HazelcastResponseEntry()), EnvoyException); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h index a62477a7e4b8e..9ca0a2f3bdae5 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h @@ -198,9 +198,11 @@ class LocalTestAccessor : public StorageAccessor, public TestAccessor { } } - bool isRunning() override { return connected_; } + bool isRunning() const override { return connected_; } - std::string clusterName() override { return "LocalTestAccessor"; } + std::string clusterName() const override { return "LocalTestAccessor"; } + + std::string startInfo() const override { return ""; } void connect() override { connected_ = true; } From a1167a1f8213c0ff8eea74ffd568028c472c3e9f Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Sat, 13 Jun 2020 18:25:45 +0300 Subject: [PATCH 30/33] Pass accessor via cache::start Signed-off-by: Enes Ozcan --- .../hazelcast_http_cache.cc | 22 ++--- .../hazelcast_http_cache.h | 21 +++-- .../hazelcast_storage_accessor.cc | 17 ++-- .../hazelcast_storage_accessor.h | 5 +- .../http/cache/hazelcast_http_cache/util.h | 6 ++ .../http/cache/hazelcast_http_cache/BUILD | 1 - .../hazelcast_common_cache_test.cc | 20 +++-- .../hazelcast_divided_cache_test.cc | 66 +++++++------- .../hazelcast_unified_cache_test.cc | 18 ++-- .../hazelcast_http_cache/test_accessors.h | 4 +- .../cache/hazelcast_http_cache/test_caches.h | 89 ------------------- .../cache/hazelcast_http_cache/test_util.h | 16 +++- 12 files changed, 106 insertions(+), 179 deletions(-) delete mode 100644 test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc index eb2b603c273b4..f5d4b49818dbc 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.cc @@ -56,21 +56,14 @@ void HazelcastHttpCache::onVersionMismatch(uint64_t key_hash, int32_t version, u onMissingBody(key_hash, version, body_size); } -void HazelcastHttpCache::start() { +void HazelcastHttpCache::start(StorageAccessorPtr&& accessor) { if (accessor_ && accessor_->isRunning()) { ENVOY_LOG(warn, "Client is already connected. Cluster name: {}", accessor_->clusterName()); return; } - ClientConfig client_config = ConfigUtil::getClientConfig(cache_config_); - client_config.getSerializationConfig().addDataSerializableFactory( - HazelcastCacheEntrySerializableFactory::FACTORY_ID, - boost::shared_ptr(new HazelcastCacheEntrySerializableFactory())); - if (!accessor_) { - accessor_ = std::make_unique( - *this, std::move(client_config), cache_config_.app_prefix(), body_partition_size_); - ENVOY_LOG(debug, "New HazelcastClusterAccessor created."); + accessor_ = std::move(accessor); } try { @@ -147,10 +140,13 @@ ProtobufTypes::MessagePtr HazelcastHttpCacheFactory::createEmptyConfigProto() { HttpCache& HazelcastHttpCacheFactory::getCache( const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config) { if (!cache_) { - HazelcastHttpCacheConfig hz_cache_config; - MessageUtil::unpackTo(config.typed_config(), hz_cache_config); - cache_ = std::make_unique(std::move(hz_cache_config), config); - cache_->start(); + HazelcastHttpCacheConfig typed_config; + MessageUtil::unpackTo(config.typed_config(), typed_config); + ClientConfig client_config = ConfigUtil::getClientConfig(typed_config); + cache_ = std::make_unique(std::move(typed_config), config); + StorageAccessorPtr accessor = std::make_unique( + *cache_, std::move(client_config), cache_->prefix(), cache_->bodySizePerEntry()); + cache_->start(std::move(accessor)); } return *cache_; } diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 3da4f1b322fe5..46bcd5cc00548 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -175,15 +175,16 @@ class HazelcastHttpCache : public HttpCache, bool unified() const { return unified_; } + const std::string& prefix() const { return cache_config_.app_prefix(); } + /** - * Makes the cache ready to serve. Storage accessor connection must be established - * via StorageAccessor::connect() when the cache is started. - * - * @note Keeping this virtual allows tests to override access strategy. - * Using a local accessor will make the cache behavior testable without - * starting a Hazelcast instance. + * Makes the cache ready to serve using the accessor. + * @param accessor Accessor to cache storage + * @note The accessor passed by the factory must establish a Hazelcast cluster + * connection. Other local storage accessors might be used during tests to + * test the cache behavior without running a real Hazelcast instance. */ - virtual void start(); + void start(StorageAccessorPtr&& accessor); /** * Drops accessor connection to the storage. @@ -200,12 +201,8 @@ class HazelcastHttpCache : public HttpCache, ~HazelcastHttpCache() override; -protected: - std::unique_ptr accessor_; - private: friend class HazelcastHttpCacheTestBase; - friend class HazelcastRemoteTestCache; /** * Generates a Hazelcast map key from the hash of the filter's cache key. @@ -236,6 +233,8 @@ class HazelcastHttpCache : public HttpCache, return std::to_string(key_hash).append("#").append(std::to_string(order)); } + StorageAccessorPtr accessor_; + /** Cache mode */ const bool unified_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc index 9087b51f89408..b6bf451d176b1 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.cc @@ -99,18 +99,17 @@ void HazelcastClusterAccessor::connect() { } std::string HazelcastClusterAccessor::startInfo() const { - return absl::StrFormat("HazelcastHttpCache is created with profile: %s. Max body size: %d.\n" - "Cache statistics can be observed on Hazelcast Management Center " - "from the map named %s.", - cache_.unified() ? - "UNIFIED" : - "DIVIDED, partition size: " + std::to_string(partition_size_), - cache_.maxBodyBytes(), - cache_.unified() ? response_map_name_ : header_map_name_); + return absl::StrFormat( + "HazelcastHttpCache is created with profile: %s. Max body size: %d.\n" + "Cache statistics can be observed on Hazelcast Management Center " + "from the map named %s.", + cache_.unified() ? "UNIFIED" : "DIVIDED, partition size: " + std::to_string(partition_size_), + cache_.maxBodyBytes(), cache_.unified() ? response_map_name_ : header_map_name_); } std::string HazelcastClusterAccessor::constructMapName(const std::string& postfix, bool unified) { - return absl::StrFormat("%s-%d-%s", app_prefix_, unified ? cache_.maxBodyBytes() : partition_size_, postfix); + return absl::StrFormat("%s-%d-%s", app_prefix_, unified ? cache_.maxBodyBytes() : partition_size_, + postfix); } void HeaderMapEntryListener::entryEvicted(const EntryEvent& event) { diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h index 0fcd17cad70d1..4037846f1d14c 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_storage_accessor.h @@ -2,9 +2,9 @@ #include "envoy/common/exception.h" -#include "absl/strings/str_format.h" #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" +#include "absl/strings/str_format.h" #include "hazelcast/client/HazelcastClient.h" #include "hazelcast/client/IMap.h" @@ -48,6 +48,7 @@ class StorageAccessor { virtual ~StorageAccessor() = default; }; +using StorageAccessorPtr = std::unique_ptr; class HazelcastHttpCache; class HeaderMapEntryListener; @@ -56,7 +57,7 @@ class HeaderMapEntryListener; * * The cache uses this accessor in the production code. */ -class HazelcastClusterAccessor : public StorageAccessor { +class HazelcastClusterAccessor : public virtual StorageAccessor { public: HazelcastClusterAccessor(HazelcastHttpCache& cache, ClientConfig&& client_config, const std::string& app_prefix, const uint64_t partition_size); diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h index 9be859b8adc39..15eeb1d3a529f 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -2,6 +2,8 @@ #include "source/extensions/filters/http/cache/hazelcast_http_cache/config.pb.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_cache_entry.h" + #include "hazelcast/client/ClientConfig.h" namespace Envoy { @@ -58,6 +60,10 @@ class ConfigUtil { std::to_string(cache_config.invocation_timeout() == 0 ? DEFAULT_INVOCATION_TIMEOUT_SEC : cache_config.invocation_timeout())); + + config.getSerializationConfig().addDataSerializableFactory( + HazelcastCacheEntrySerializableFactory::FACTORY_ID, + boost::shared_ptr(new HazelcastCacheEntrySerializableFactory())); return config; } diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 26abff2824ac7..9982ea24ee8e4 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -58,7 +58,6 @@ envoy_extension_cc_test_library( name = "hazelcast_test_lib", hdrs = [ "test_accessors.h", - "test_caches.h", "test_util.h", ], extension_name = "envoy.filters.http.cache.hazelcast_http_cache", diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index 5533e4fab760f..e7963e581d8ea 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -20,11 +20,11 @@ class HazelcastHttpCacheTest : public HazelcastHttpCacheTestBase, HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(GetParam()); envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); - // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(std::move(typed_config), cache_config); - cache_ = std::make_unique(std::move(typed_config), cache_config); - cache_->start(); - cache_->getTestAccessor().clearMaps(); + cache_ = std::make_unique(std::move(typed_config), cache_config); + // To test the cache with a real Hazelcast instance, use remote test accessor. + // cache_->start(HazelcastTestUtil::getTestRemoteAccessor(*cache_)); + cache_->start(std::make_unique()); + getTestAccessor().clearMaps(); } Key getVariantKey(LookupContextPtr& lookup, @@ -152,7 +152,7 @@ TEST_P(HazelcastHttpCacheTest, Miss) { EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // Do not left over a missed lookup without inserting or releasing its lock. - // This is required for HazelcastRemoteTestCache. + // This is required for remote accessor. cache_->unlock(variant_key_hash); } @@ -223,16 +223,20 @@ TEST(Registration, GetFactory) { typed_config.set_group_name("do-not-connect-any-cluster"); typed_config.set_connection_attempt_limit(1); typed_config.set_connection_attempt_period(1); // give up immediately. + ClientConfig client_config = ConfigUtil::getClientConfig(typed_config); config.mutable_typed_config()->PackFrom(typed_config); { // getOfflineCache() call is for testing. It creates a HazelcastHttpCache but does // not make it operational until a start() call. This is required to make cacheInfo() - // behavior testable when using HazelcastLocalTestCache. + // behavior testable when using local accessor. HazelcastHttpCachePtr cache = static_cast(factory)->getOfflineCache(config); EXPECT_EQ(cache->cacheInfo().name_, "envoy.extensions.http.cache.hazelcast"); - EXPECT_THROW_WITH_MESSAGE(cache->start(), EnvoyException, + + StorageAccessorPtr accessor = std::make_unique( + *cache, std::move(client_config), cache->prefix(), cache->bodySizePerEntry()); + EXPECT_THROW_WITH_MESSAGE(cache->start(std::move(accessor)), EnvoyException, "Hazelcast Client could not connect to any cluster."); } EXPECT_THROW_WITH_MESSAGE(factory->getCache(config), EnvoyException, diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 6ca5063a5f3f6..96f02bf211ba6 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -19,11 +19,11 @@ class HazelcastDividedCacheTest : public HazelcastHttpCacheTestBase { HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(false); envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); - // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(std::move(typed_config), cache_config); - cache_ = std::make_unique(std::move(typed_config), cache_config); - cache_->start(); - cache_->getTestAccessor().clearMaps(); + cache_ = std::make_unique(std::move(typed_config), cache_config); + // To test the cache with a real Hazelcast instance, use remote test accessor. + // cache_->start(HazelcastTestUtil::getTestRemoteAccessor(*cache_)); + cache_->start(std::make_unique()); + getTestAccessor().clearMaps(); } }; @@ -48,7 +48,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedInsertionWhenMaxSizeReached) { EXPECT_EQ(((HazelcastTestUtil::TEST_MAX_BODY_SIZE + HazelcastTestUtil::TEST_PARTITION_SIZE - 1) / HazelcastTestUtil::TEST_PARTITION_SIZE), - cache_->getTestAccessor().bodyMapSize()); + getTestAccessor().bodyMapSize()); EXPECT_TRUE(expectLookupSuccessWithFullBody( lookup(RequestPath).get(), std::string(HazelcastTestUtil::TEST_MAX_BODY_SIZE, 'h'))); } @@ -75,8 +75,8 @@ TEST_F(HazelcastDividedCacheTest, CleanBodyOnHeaderEviction) { const int BodyCount = 3; insert(move(lookup_context), getResponseHeaders(), std::string(HazelcastTestUtil::TEST_PARTITION_SIZE * BodyCount, 'h')); - EXPECT_EQ(1, cache_->getTestAccessor().headerMapSize()); - EXPECT_EQ(BodyCount, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(1, getTestAccessor().headerMapSize()); + EXPECT_EQ(BodyCount, getTestAccessor().bodyMapSize()); auto key_object = std::make_unique(mapKey(variant_key_hash)); auto value_object = cache_->getHeader(variant_key_hash); @@ -87,7 +87,7 @@ TEST_F(HazelcastDividedCacheTest, CleanBodyOnHeaderEviction) { EXPECT_CALL(mock_event, getOldValueObject()).WillRepeatedly(testing::Return(value_object.get())); EXPECT_CALL(mock_event, getKeyObject()).WillRepeatedly(testing::Return(key_object.get())); - EXPECT_EQ(BodyCount, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(BodyCount, getTestAccessor().bodyMapSize()); HeaderMapEntryListener listener(*cache_); listener.entryAdded(mock_event); // no-op @@ -100,9 +100,9 @@ TEST_F(HazelcastDividedCacheTest, CleanBodyOnHeaderEviction) { listener.mapEvicted(null_event); // no-op listener.mapCleared(null_event); // no-op - EXPECT_EQ(BodyCount, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(BodyCount, getTestAccessor().bodyMapSize()); listener.entryEvicted(mock_event); - EXPECT_EQ(0, cache_->getTestAccessor().bodyMapSize()); + EXPECT_EQ(0, getTestAccessor().bodyMapSize()); } TEST_F(HazelcastDividedCacheTest, AbortInsertionIfKeyIsLocked) { @@ -167,8 +167,8 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { EXPECT_EQ(fullBody, HazelcastTestUtil::abortedBodyResponse()); // Clean up must be performed for malformed entries. - EXPECT_EQ(0, cache_->getTestAccessor().bodyMapSize()); - EXPECT_EQ(0, cache_->getTestAccessor().headerMapSize()); + EXPECT_EQ(0, getTestAccessor().bodyMapSize()); + EXPECT_EQ(0, getTestAccessor().headerMapSize()); } TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { @@ -203,7 +203,7 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - EXPECT_EQ(1, cache_->getTestAccessor().headerMapSize()); + EXPECT_EQ(1, getTestAccessor().headerMapSize()); auto modified_header = cache_->getHeader(variant_key_hash); EXPECT_EQ(*header, *modified_header); @@ -230,7 +230,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { // variant_key_hash "2" -> Body3 (in body map) EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); - cache_->getTestAccessor().removeBody(orderedMapKey(variant_key_hash, 1)); // evict Body2. + getTestAccessor().removeBody(orderedMapKey(variant_key_hash, 1)); // evict Body2. lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); @@ -253,7 +253,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); - EXPECT_NE(0, cache_->getTestAccessor().bodyMapSize()); // clean up is not performed. + EXPECT_NE(0, getTestAccessor().bodyMapSize()); // clean up is not performed. cache_->unlock(variant_key_hash); } @@ -270,7 +270,7 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { {HazelcastTestUtil::TEST_PARTITION_SIZE, HazelcastTestUtil::TEST_PARTITION_SIZE * 3}, [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); - EXPECT_NE(0, cache_->getTestAccessor().bodyMapSize()); + EXPECT_NE(0, getTestAccessor().bodyMapSize()); header->version(original_version); cache_->putHeader(variant_key_hash, *header); @@ -286,12 +286,12 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); // Assert clean up - EXPECT_EQ(0, cache_->getTestAccessor().bodyMapSize()); - EXPECT_EQ(0, cache_->getTestAccessor().headerMapSize()); + EXPECT_EQ(0, getTestAccessor().bodyMapSize()); + EXPECT_EQ(0, getTestAccessor().headerMapSize()); } // Cache must handle the connection failure during clean up. - cache_->getTestAccessor().failOnLock(); + getTestAccessor().failOnLock(); cache_->onMissingBody(0, 0, 0); // HazelcastClientOfflineException cache_->onMissingBody(0, 0, 0); // std::exception } @@ -313,8 +313,8 @@ TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { // then empty body for body insertion. headerOnlyTest("/empty/body/response", true); - EXPECT_EQ(0, cache_->getTestAccessor().bodyMapSize()); - EXPECT_EQ(2, cache_->getTestAccessor().headerMapSize()); + EXPECT_EQ(0, getTestAccessor().bodyMapSize()); + EXPECT_EQ(2, getTestAccessor().headerMapSize()); } TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { @@ -330,7 +330,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { lookup_context = lookup(RequestPath); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); - cache_->getTestAccessor().dropConnection(); + getTestAccessor().dropConnection(); // std::exception case. lookup_context = lookup(RequestPath); @@ -346,7 +346,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { insert(move(lookup_context), getResponseHeaders(), Body); - cache_->getTestAccessor().restoreConnection(); + getTestAccessor().restoreConnection(); lookup_context = lookup(RequestPath); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); @@ -364,7 +364,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'h'), false); insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'z'), false); - cache_->getTestAccessor().dropConnection(); + getTestAccessor().dropConnection(); // testing std::exception case. insert(std::string(HazelcastTestUtil::TEST_PARTITION_SIZE, 'c'), true); @@ -376,7 +376,7 @@ TEST_F(HazelcastDividedCacheTest, AbortDividedOperationsWhenOffline) { LookupContextPtr lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - cache_->getTestAccessor().restoreConnection(); + getTestAccessor().restoreConnection(); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -393,19 +393,19 @@ TEST_F(HazelcastDividedCacheTest, FailDuringLock) { static_cast(*lookup(RequestPath)).variantKeyHash(); std::thread t1([&] { - // To make this test compatible with HazelcastRemoteTestCache, the key is locked here + // To make this test compatible with remote accessor, the key is locked here // explicitly. This behavior will cause tryLock to return false for further // trials. Hence the lookups below will throw exception for local cache and // return false for remote cache. This has no effect on local test but lack of // this locking causes remote test to fail. Notice that if this locking would not // be performed by a different thread, following insertions in this thread would - // be able to lock the key again and again for HazelcastRemoteTestCache. + // be able to lock the key again and again for remote accessor. EXPECT_TRUE(cache_->tryLock(variant_key_hash)); }); t1.join(); // This will cause LocalTestAccessor::tryLock to raise error. - cache_->getTestAccessor().failOnLock(); + getTestAccessor().failOnLock(); insert(lookup(RequestPath), getResponseHeaders(), "aborted"); // std::exception insert(lookup(RequestPath), getResponseHeaders(), "aborted"); // HazelcastClientOfflineException @@ -432,7 +432,7 @@ TEST_F(HazelcastDividedCacheTest, FailDuringBodyLookupWhenHeaderSucceeds) { lookup_context->getBody({0, HazelcastTestUtil::TEST_PARTITION_SIZE}, [](Buffer::InstancePtr&& data) { EXPECT_NE(data, nullptr); }); - cache_->getTestAccessor().dropConnection(); + getTestAccessor().dropConnection(); // Lookup for body should be aborted on HazelcastOffline exception. lookup_context->getBody({0, body_size}, @@ -446,7 +446,7 @@ TEST_F(HazelcastDividedCacheTest, FailDuringBodyLookupWhenHeaderSucceeds) { lookup_context->getBody({0, body_size}, [](Buffer::InstancePtr&& data) { EXPECT_EQ(data, nullptr); }); - cache_->getTestAccessor().restoreConnection(); + getTestAccessor().restoreConnection(); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context.get(), Body)); } @@ -465,10 +465,10 @@ TEST_F(HazelcastDividedCacheTest, AbortInsertionWhenLockLeftover) { InsertContextPtr insert_context1 = cache_->makeInsertContext(std::move(lookup_context1)); InsertContextPtr insert_context2 = cache_->makeInsertContext(std::move(lookup_context2)); - cache_->getTestAccessor().dropConnection(); + getTestAccessor().dropConnection(); insert_context1->insertHeaders(getResponseHeaders(), true); insert_context2->insertHeaders(getResponseHeaders(), true); - cache_->getTestAccessor().restoreConnection(); + getTestAccessor().restoreConnection(); lookup_context1 = lookup(RequestPath1); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index 589f51f9bda9b..c55dc8df6eab2 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -18,11 +18,11 @@ class HazelcastUnifiedCacheTest : public HazelcastHttpCacheTestBase { HazelcastHttpCacheConfig typed_config = HazelcastTestUtil::getTestTypedConfig(true); envoy::extensions::filters::http::cache::v3alpha::CacheConfig cache_config = HazelcastTestUtil::getTestCacheConfig(); - // To test the cache with a real Hazelcast instance, use remote test cache. - // cache_ = std::make_unique(std::move(typed_config), cache_config); - cache_ = std::make_unique(std::move(typed_config), cache_config); - cache_->start(); - cache_->getTestAccessor().clearMaps(); + cache_ = std::make_unique(std::move(typed_config), cache_config); + // To test the cache with a real Hazelcast instance, use remote test accessor. + // cache_->start(HazelcastTestUtil::getTestRemoteAccessor(*cache_)); + cache_->start(std::make_unique()); + getTestAccessor().clearMaps(); } }; @@ -91,7 +91,7 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { modified.add_custom_fields("custom1"); modified.add_custom_fields("custom2"); response->header().variantKey(std::move(modified)); - cache_->getTestAccessor().insertResponse(mapKey(variant_key_hash), *response); + getTestAccessor().insertResponse(mapKey(variant_key_hash), *response); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); @@ -102,7 +102,7 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { insert(move(lookup_context), getResponseHeaders(), Body); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - EXPECT_EQ(1, cache_->getTestAccessor().responseMapSize()); + EXPECT_EQ(1, getTestAccessor().responseMapSize()); } TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { @@ -121,7 +121,7 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { LookupContextPtr succeed_lookup2 = lookup(RequestPath1); LookupContextPtr succeed_lookup3 = lookup(RequestPath1); - cache_->getTestAccessor().dropConnection(); + getTestAccessor().dropConnection(); // UnifiedLookupContext::getHeaders when HazelcastClientOfflineException is thrown. lookup_context1 = lookup(RequestPath1); @@ -144,7 +144,7 @@ TEST_F(HazelcastUnifiedCacheTest, AbortUnifiedOperationsWhenOffline) { // UnifiedInsertContext::insertResponse when std::exception is thrown. insert(move(succeed_lookup3), getResponseHeaders(), Body); - cache_->getTestAccessor().restoreConnection(); + getTestAccessor().restoreConnection(); lookup_context1 = lookup(RequestPath1); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup_context1.get(), Body)); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h index 9ca0a2f3bdae5..5effc914cdefa 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h @@ -14,7 +14,7 @@ namespace HazelcastHttpCache { * Contains pure functions to obtain storage information, modify the storage and * change accessor behavior directly. */ -class TestAccessor { +class TestAccessor : public virtual StorageAccessor { public: TestAccessor() = default; @@ -76,7 +76,7 @@ class RemoteTestAccessor : public TestAccessor, public HazelcastClusterAccessor * @note This accessor does not use any Hazelcast instance during tests. * Instead, it simulates Hazelcast instance with local storage. */ -class LocalTestAccessor : public StorageAccessor, public TestAccessor { +class LocalTestAccessor : public TestAccessor { public: LocalTestAccessor() = default; diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h deleted file mode 100644 index 87db42d85b94e..0000000000000 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" - -#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h" - -namespace Envoy { -namespace Extensions { -namespace HttpFilters { -namespace Cache { -namespace HazelcastHttpCache { - -/** - * Base testable cache for both local and remote ones. - * - * Exposes accessor to extend testing flexibility. Storage control can be achieved - * directly via test accessors. - */ -class HazelcastHttpTestCache : public HazelcastHttpCache { -public: - HazelcastHttpTestCache( - HazelcastHttpCacheConfig&& typed_config, - const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) - : HazelcastHttpCache(std::move(typed_config), cache_config) {} - - TestAccessor& getTestAccessor() { return dynamic_cast(*accessor_); } -}; - -/** - * Testable cache with RemoteTestAccessor. - * - * Requires a running Hazelcast instance to be tested. - */ -class HazelcastRemoteTestCache : public HazelcastHttpTestCache { -public: - HazelcastRemoteTestCache( - HazelcastHttpCacheConfig&& typed_config, - const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) - : HazelcastHttpTestCache(std::move(typed_config), cache_config) {} - - void start() override { - if (accessor_ && accessor_->isRunning()) { - return; - } - - ClientConfig client_config = ConfigUtil::getClientConfig(cache_config_); - client_config.getSerializationConfig().addDataSerializableFactory( - HazelcastCacheEntrySerializableFactory::FACTORY_ID, - boost::shared_ptr(new HazelcastCacheEntrySerializableFactory())); - - if (!accessor_) { - accessor_ = std::make_unique( - *this, std::move(client_config), cache_config_.app_prefix(), body_partition_size_); - } - - try { - accessor_->connect(); - } catch (...) { - throw EnvoyException("Hazelcast Client could not connect to any cluster."); - } - } -}; - -/** - * Testable cache with LocalTestAccessor. - * - * Does not require a running Hazelcast instance. Instead, tests the cache - * with local storage. This is the way the cache is tested in CI environment. - */ -class HazelcastLocalTestCache : public HazelcastHttpTestCache { -public: - HazelcastLocalTestCache( - HazelcastHttpCacheConfig&& typed_config, - const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& cache_config) - : HazelcastHttpTestCache(std::move(typed_config), cache_config) {} - - void start() override { - if (!accessor_) { - accessor_ = std::make_unique(); - } - accessor_->connect(); - } -}; - -} // namespace HazelcastHttpCache -} // namespace Cache -} // namespace HttpFilters -} // namespace Extensions -} // namespace Envoy diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h index 0cbe62f1a66a0..a60b583634733 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h @@ -1,6 +1,8 @@ #pragma once -#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_caches.h" +#include "extensions/filters/http/cache/hazelcast_http_cache/util.h" + +#include "test/extensions/filters/http/cache/hazelcast_http_cache/test_accessors.h" #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" @@ -49,6 +51,14 @@ class HazelcastTestUtil { return typed_config; } + static StorageAccessorPtr getTestRemoteAccessor(HazelcastHttpCache& cache) { + HazelcastHttpCacheConfig typed_config = getTestTypedConfig(cache.unified()); + ClientConfig client_config = ConfigUtil::getClientConfig(typed_config); + StorageAccessorPtr accessor = std::make_unique( + cache, std::move(client_config), cache.prefix(), cache.bodySizePerEntry()); + return accessor; + } + static void setRequestHeaders(Http::TestRequestHeaderMapImpl& headers) { headers.setMethod("GET"); headers.setHost("hazelcast.com"); @@ -70,6 +80,8 @@ class HazelcastHttpCacheTestBase : public testing::Test { int64_t mapKey(const uint64_t key_hash) { return cache_->mapKey(key_hash); } + TestAccessor& getTestAccessor() { return dynamic_cast(*cache_->accessor_); } + std::string orderedMapKey(const uint64_t key_hash, const uint64_t order) { return cache_->orderedMapKey(key_hash, order); } @@ -161,7 +173,7 @@ class HazelcastHttpCacheTestBase : public testing::Test { return AssertionSuccess(); } - std::unique_ptr cache_; + std::unique_ptr cache_; LookupResult lookup_result_; Http::TestRequestHeaderMapImpl request_headers_; Event::SimulatedTimeSystem time_source_; From 639bfbb25af477da2151d37a85ed172a9a76b503 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Sat, 13 Jun 2020 18:31:35 +0300 Subject: [PATCH 31/33] Revert back to unit64 max body bytes. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_http_cache/hazelcast_http_cache.h | 4 ++-- .../extensions/filters/http/cache/hazelcast_http_cache/util.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h index 46bcd5cc00548..724b002e33bbd 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_http_cache.h @@ -171,7 +171,7 @@ class HazelcastHttpCache : public HttpCache, * than this limit, the first max_body_size_ bytes of the response * will be cached only. */ - uint32_t maxBodyBytes() const { return max_body_bytes_; } + uint64_t maxBodyBytes() const { return max_body_bytes_; } bool unified() const { return unified_; } @@ -242,7 +242,7 @@ class HazelcastHttpCache : public HttpCache, const uint64_t body_partition_size_; /** Allowed max body size for a response */ - const uint32_t max_body_bytes_; + const uint64_t max_body_bytes_; /** typed config from CacheConfig */ HazelcastHttpCacheConfig cache_config_; diff --git a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h index 15eeb1d3a529f..dc531458a57f9 100644 --- a/source/extensions/filters/http/cache/hazelcast_http_cache/util.h +++ b/source/extensions/filters/http/cache/hazelcast_http_cache/util.h @@ -21,7 +21,7 @@ class ConfigUtil { : config_value; } - static uint32_t validMaxBodySize(const uint32_t config_value, const bool unified) { + static uint64_t validMaxBodySize(const uint64_t config_value, const bool unified) { if (unified) { // Apply size limitation for single entry (unified response) on the map. return config_value == 0 || (config_value > MAX_ALLOWED_UNIFIED_BODY_SIZE) From ab2f3fd9ca85c161987c623094596de90df65eb8 Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Sat, 13 Jun 2020 22:08:55 +0300 Subject: [PATCH 32/33] Add tests. Signed-off-by: Enes Ozcan --- .../http/cache/hazelcast_http_cache/BUILD | 1 + .../hazelcast_common_cache_test.cc | 9 +++++++++ .../hazelcast_divided_cache_test.cc | 19 +++++++++++++------ .../hazelcast_unified_cache_test.cc | 12 +++++++++--- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD index 9982ea24ee8e4..28a03a79c004f 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/BUILD @@ -63,6 +63,7 @@ envoy_extension_cc_test_library( extension_name = "envoy.filters.http.cache.hazelcast_http_cache", deps = [ "//source/extensions/filters/http/cache/hazelcast_http_cache:hazelcast_http_cache_lib", + "//test/test_common:logging_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", ], diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc index e7963e581d8ea..2cbf643b17b8e 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_common_cache_test.cc @@ -3,6 +3,7 @@ #include "extensions/filters/http/cache/hazelcast_http_cache/hazelcast_context.h" #include "test/extensions/filters/http/cache/hazelcast_http_cache/test_util.h" +#include "test/test_common/logging.h" namespace Envoy { namespace Extensions { @@ -63,6 +64,7 @@ TEST_P(HazelcastHttpCacheTest, MissPutAndGetEntries) { EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath1).get(), Body1)); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath2).get(), Body2)); EXPECT_TRUE(expectLookupSuccessWithFullBody(lookup(RequestPath3).get(), Body3)); + EXPECT_EQ(GetParam(), cache_->unified()); } TEST_P(HazelcastHttpCacheTest, HandleRangedResponses) { @@ -214,6 +216,13 @@ TEST_P(HazelcastHttpCacheTest, StreamingPutAndRangeGet) { EXPECT_EQ("o, World!", getBody(*name_lookup_context, 4, 13)); } +TEST_P(HazelcastHttpCacheTest, CacheStartShutdown) { + EXPECT_LOG_CONTAINS("warn", "Client is already connected.", + cache_->start(std::make_unique())); + cache_->shutdown(false); + EXPECT_LOG_CONTAINS("warn", "Hazelcast client is already disconnected.", cache_->shutdown(true)); +} + TEST(Registration, GetFactory) { HttpCacheFactory* factory = Registry::FactoryRegistry::getFactoryByType( "envoy.source.extensions.filters.http.cache.HazelcastHttpCacheConfig"); diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc index 96f02bf211ba6..793c63c71a96b 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_divided_cache_test.cc @@ -172,7 +172,6 @@ TEST_F(HazelcastDividedCacheTest, MissLookupOnVersionMismatch) { } TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { - const std::string RequestPath("/miss/on/different/key"); LookupContextPtr lookup_context = lookup(RequestPath); @@ -199,8 +198,11 @@ TEST_F(HazelcastDividedCacheTest, MissDividedLookupOnDifferentKey) { // New entry insertion should be aborted and not override the existing one with the // same hash key. This scenario is possible if there is a hash collision. No eviction - // or clean up is expected. Since overriding an entry is prevented. - insert(move(lookup_context), getResponseHeaders(), Body); + // or clean up is expected. Since overriding an entry is prevented in this case. + InsertContextPtr insert_context = cache_->makeInsertContext(std::move(lookup_context)); + insert_context->insertHeaders(getResponseHeaders(), false); + insert_context->insertBody( + Buffer::OwnedImpl(Body), [](bool ready) { EXPECT_FALSE(ready); }, true); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); EXPECT_EQ(1, getTestAccessor().headerMapSize()); @@ -297,11 +299,16 @@ TEST_F(HazelcastDividedCacheTest, CleanUpCachedResponseOnMissingBody) { } TEST_F(HazelcastDividedCacheTest, NotCreateBodyOnHeaderOnlyResponse) { - auto headerOnlyTest = [this](std::string path, bool empty_body) { + auto headerOnlyTest = [this](std::string path, bool use_empty_body) { LookupContextPtr lookup_context = lookup(path); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); - insert(move(lookup_context), getResponseHeaders(), empty_body ? "" : nullptr); - lookup_context = lookup(path); + InsertContextPtr insert_context = cache_->makeInsertContext(std::move(lookup_context)); + insert_context->insertHeaders(getResponseHeaders(), !use_empty_body); + if (use_empty_body) { + insert_context->insertBody( + Buffer::OwnedImpl(""), [](bool) {}, true); + } + lookup(path); EXPECT_EQ(CacheEntryStatus::Ok, lookup_result_.cache_entry_status_); EXPECT_EQ(0, lookup_result_.content_length_); }; diff --git a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc index c55dc8df6eab2..2d75e94cbf492 100644 --- a/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc +++ b/test/extensions/filters/http/cache/hazelcast_http_cache/hazelcast_unified_cache_test.cc @@ -70,7 +70,6 @@ TEST_F(HazelcastUnifiedCacheTest, UnifiedHeaderOnlyResponse) { } TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { - const std::string RequestPath("/miss/on/different/key"); LookupContextPtr lookup_context = lookup(RequestPath); @@ -98,8 +97,11 @@ TEST_F(HazelcastUnifiedCacheTest, MissUnifiedLookupOnDifferentKey) { // New entry insertion should be aborted and not override the existing one with the // same hash key. This scenario is possible if there is a hash collision. No eviction - // or clean up is expected. Since overriding an entry is prevented. - insert(move(lookup_context), getResponseHeaders(), Body); + // or clean up is expected. Since overriding an entry is prevented in this case. + InsertContextPtr insert_context = cache_->makeInsertContext(std::move(lookup_context)); + insert_context->insertHeaders(getResponseHeaders(), false); + insert_context->insertBody( + Buffer::OwnedImpl(Body), [](bool ready) { EXPECT_FALSE(ready); }, true); lookup_context = lookup(RequestPath); EXPECT_EQ(CacheEntryStatus::Unusable, lookup_result_.cache_entry_status_); EXPECT_EQ(1, getTestAccessor().responseMapSize()); @@ -156,6 +158,10 @@ TEST_F(HazelcastUnifiedCacheTest, CoverRemoteOperations) { // covers the calls made to Hazelcast cluster. Otherwise the coverage // stays at 90% without including the remote calls. HazelcastClusterAccessor accessor(*cache_, ClientConfig(), "coverage", 10); + const std::string info = accessor.startInfo(); + EXPECT_NE(info.find("profile: UNIFIED"), std::string::npos); + EXPECT_NE(info.find(absl::StrFormat("Max body size: %d", cache_->maxBodyBytes())), + std::string::npos); EXPECT_FALSE(accessor.isRunning()); EXPECT_STREQ("", accessor.clusterName().c_str()); EXPECT_THROW(accessor.putHeader(1, HazelcastHeaderEntry()), EnvoyException); From 49ca85b10493cc93d1ef257772247f9ebae694fe Mon Sep 17 00:00:00 2001 From: Enes Ozcan Date: Sat, 13 Jun 2020 22:10:34 +0300 Subject: [PATCH 33/33] Add Windows build defines. Signed-off-by: Enes Ozcan --- bazel/foreign_cc/BUILD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bazel/foreign_cc/BUILD b/bazel/foreign_cc/BUILD index 84b7d62e46636..f0cc86f26be52 100644 --- a/bazel/foreign_cc/BUILD +++ b/bazel/foreign_cc/BUILD @@ -193,8 +193,10 @@ envoy_cmake_external( name = "hazelcast_cpp_client", cache_entries = { "HZ_LIB_TYPE": "STATIC", + "HZ_BIT": "64", "CMAKE_BUILD_TYPE": "RELEASE", }, + defines = ["HAZELCAST_USE_STATIC"], lib_source = "@com_github_hazelcast_cpp_client//:all", static_libraries = select({ "//bazel:windows_x86_64": ["HazelcastClient3.12.1_64.lib"],