diff --git a/CODEOWNERS b/CODEOWNERS index 601f9c69755dd..74585e07c03b9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -68,6 +68,8 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/common/aws @lavignes @mattklein123 # adaptive concurrency limit extension. /*/extensions/filters/http/adaptive_concurrency @tonya11en @mattklein123 +# admission control extension. +/*/extensions/filters/http/admission_control @tonya11en @mattklein123 # http inspector /*/extensions/filters/listener/http_inspector @yxue @PiotrSikora @lizan # attribute context diff --git a/api/BUILD b/api/BUILD index c701bdcf48330..45cd829390f9b 100644 --- a/api/BUILD +++ b/api/BUILD @@ -165,6 +165,7 @@ proto_library( "//envoy/extensions/compression/gzip/decompressor/v3:pkg", "//envoy/extensions/filters/common/fault/v3:pkg", "//envoy/extensions/filters/http/adaptive_concurrency/v3:pkg", + "//envoy/extensions/filters/http/admission_control/v3alpha:pkg", "//envoy/extensions/filters/http/aws_lambda/v3:pkg", "//envoy/extensions/filters/http/aws_request_signing/v3:pkg", "//envoy/extensions/filters/http/buffer/v3:pkg", diff --git a/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto b/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto index 3d2ef3e96d968..8dd851f4020a5 100644 --- a/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto +++ b/api/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto @@ -5,7 +5,6 @@ package envoy.extensions.filters.http.adaptive_concurrency.v3; import "envoy/config/core/v3/base.proto"; import "envoy/type/v3/percent.proto"; -import "google/api/annotations.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; diff --git a/api/envoy/extensions/filters/http/admission_control/v3alpha/BUILD b/api/envoy/extensions/filters/http/admission_control/v3alpha/BUILD new file mode 100644 index 0000000000000..f139cce54af25 --- /dev/null +++ b/api/envoy/extensions/filters/http/admission_control/v3alpha/BUILD @@ -0,0 +1,13 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/admission_control/v3alpha/admission_control.proto b/api/envoy/extensions/filters/http/admission_control/v3alpha/admission_control.proto new file mode 100644 index 0000000000000..6f77ca9886237 --- /dev/null +++ b/api/envoy/extensions/filters/http/admission_control/v3alpha/admission_control.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.admission_control.v3alpha; + +import "envoy/config/core/v3/base.proto"; +import "envoy/type/v3/range.proto"; + +import "google/api/annotations.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; +import "google/rpc/status.proto"; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.admission_control.v3alpha"; +option java_outer_classname = "AdmissionControlProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).work_in_progress = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Admission Control] +// [#extension: envoy.filters.http.admission_control] + +message AdmissionControl { + // Default method of specifying what constitutes a failed request. All status codes that + // indicate a failed request must be explicitly specified if not relying on the default + // values. + message DefaultEvaluationCriteria { + // If HTTP statuses are unspecified, defaults to 5xx. + repeated type.v3.Int32Range http_status = 1; + + // GRPC status codes to consider as request successes. If unspecified, defaults to "OK". + repeated uint32 grpc_status = 2; + } + + // If set to false, the admission control filter will operate as a pass-through filter. If the + // message is unspecified, the filter will be enabled. + config.core.v3.RuntimeFeatureFlag enabled = 1; + + // Defines how a request is considered a success/failure. + oneof evaluation_criteria { + option (validate.required) = true; + + DefaultEvaluationCriteria default_eval_criteria = 2; + } + + // The sliding time window over which the success rate is calculated. The window is rounded to the + // nearest second. Defaults to 120s. + google.protobuf.Duration sampling_window = 3; + + // Rejection probability is defined by the formula:: + // + // max(0, (rq_count - aggression_coefficient * rq_success_count) / (rq_count + 1)) + // + // The coefficient dictates how aggressively the admission controller will throttle requests as + // the success rate drops. Lower values will cause throttling to kick in at higher success rates + // and result in more aggressive throttling. Any values less than 1.0, will be set to 1.0. If the + // message is unspecified, the coefficient is 2.0. + config.core.v3.RuntimeDouble aggression_coefficient = 4; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index d7771fbbd29ec..c72bffdb67a8e 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -48,6 +48,7 @@ proto_library( "//envoy/extensions/compression/gzip/decompressor/v3:pkg", "//envoy/extensions/filters/common/fault/v3:pkg", "//envoy/extensions/filters/http/adaptive_concurrency/v3:pkg", + "//envoy/extensions/filters/http/admission_control/v3alpha:pkg", "//envoy/extensions/filters/http/aws_lambda/v3:pkg", "//envoy/extensions/filters/http/aws_request_signing/v3:pkg", "//envoy/extensions/filters/http/buffer/v3:pkg", diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index 3d541aed13cf3..9cb986055c22f 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -40,4 +40,5 @@ HTTP filters .. toctree:: :hidden: + ../../../api-v3/extensions/filters/http/admission_control/v3alpha/admission_control.proto ../../../api-v3/extensions/filters/http/cache/v3alpha/cache.proto diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index 15ac05d10cede..9ae658780070b 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -34,6 +34,7 @@ proto_library( "//envoy/config/filter/dubbo/router/v2alpha1:pkg", "//envoy/config/filter/fault/v2:pkg", "//envoy/config/filter/http/adaptive_concurrency/v2alpha:pkg", + "//envoy/config/filter/http/admission_control/v2alpha:pkg", "//envoy/config/filter/http/buffer/v2:pkg", "//envoy/config/filter/http/compressor/v2:pkg", "//envoy/config/filter/http/cors/v2:pkg", @@ -129,6 +130,7 @@ proto_library( "//envoy/extensions/common/tap/v3:pkg", "//envoy/extensions/filters/common/fault/v3:pkg", "//envoy/extensions/filters/http/adaptive_concurrency/v3:pkg", + "//envoy/extensions/filters/http/admission_control/v3alpha:pkg", "//envoy/extensions/filters/http/buffer/v3:pkg", "//envoy/extensions/filters/http/compressor/v3:pkg", "//envoy/extensions/filters/http/cors/v3:pkg", diff --git a/generated_api_shadow/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto b/generated_api_shadow/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto index 3d2ef3e96d968..8dd851f4020a5 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/adaptive_concurrency/v3/adaptive_concurrency.proto @@ -5,7 +5,6 @@ package envoy.extensions.filters.http.adaptive_concurrency.v3; import "envoy/config/core/v3/base.proto"; import "envoy/type/v3/percent.proto"; -import "google/api/annotations.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; diff --git a/generated_api_shadow/envoy/extensions/filters/http/admission_control/v3alpha/BUILD b/generated_api_shadow/envoy/extensions/filters/http/admission_control/v3alpha/BUILD new file mode 100644 index 0000000000000..f139cce54af25 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/admission_control/v3alpha/BUILD @@ -0,0 +1,13 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/http/admission_control/v3alpha/admission_control.proto b/generated_api_shadow/envoy/extensions/filters/http/admission_control/v3alpha/admission_control.proto new file mode 100644 index 0000000000000..6f77ca9886237 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/admission_control/v3alpha/admission_control.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.admission_control.v3alpha; + +import "envoy/config/core/v3/base.proto"; +import "envoy/type/v3/range.proto"; + +import "google/api/annotations.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; +import "google/rpc/status.proto"; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.admission_control.v3alpha"; +option java_outer_classname = "AdmissionControlProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).work_in_progress = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Admission Control] +// [#extension: envoy.filters.http.admission_control] + +message AdmissionControl { + // Default method of specifying what constitutes a failed request. All status codes that + // indicate a failed request must be explicitly specified if not relying on the default + // values. + message DefaultEvaluationCriteria { + // If HTTP statuses are unspecified, defaults to 5xx. + repeated type.v3.Int32Range http_status = 1; + + // GRPC status codes to consider as request successes. If unspecified, defaults to "OK". + repeated uint32 grpc_status = 2; + } + + // If set to false, the admission control filter will operate as a pass-through filter. If the + // message is unspecified, the filter will be enabled. + config.core.v3.RuntimeFeatureFlag enabled = 1; + + // Defines how a request is considered a success/failure. + oneof evaluation_criteria { + option (validate.required) = true; + + DefaultEvaluationCriteria default_eval_criteria = 2; + } + + // The sliding time window over which the success rate is calculated. The window is rounded to the + // nearest second. Defaults to 120s. + google.protobuf.Duration sampling_window = 3; + + // Rejection probability is defined by the formula:: + // + // max(0, (rq_count - aggression_coefficient * rq_success_count) / (rq_count + 1)) + // + // The coefficient dictates how aggressively the admission controller will throttle requests as + // the success rate drops. Lower values will cause throttling to kick in at higher success rates + // and result in more aggressive throttling. Any values less than 1.0, will be set to 1.0. If the + // message is unspecified, the coefficient is 2.0. + config.core.v3.RuntimeDouble aggression_coefficient = 4; +} diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 9bd2531dcdb7e..c217277c235b8 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -41,6 +41,7 @@ EXTENSIONS = { # "envoy.filters.http.adaptive_concurrency": "//source/extensions/filters/http/adaptive_concurrency:config", + "envoy.filters.http.admission_control": "//source/extensions/filters/http/admission_control:config", "envoy.filters.http.aws_lambda": "//source/extensions/filters/http/aws_lambda:config", "envoy.filters.http.aws_request_signing": "//source/extensions/filters/http/aws_request_signing:config", "envoy.filters.http.buffer": "//source/extensions/filters/http/buffer:config", diff --git a/source/extensions/filters/http/admission_control/BUILD b/source/extensions/filters/http/admission_control/BUILD new file mode 100644 index 0000000000000..04f88a4d86aa1 --- /dev/null +++ b/source/extensions/filters/http/admission_control/BUILD @@ -0,0 +1,53 @@ +licenses(["notice"]) # Apache 2 + +# HTTP L7 filter that probabilistically rejects requests based on upstream success-rate. +# Public docs: docs/root/configuration/http_filters/admission_control.rst + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "admission_control_filter_lib", + srcs = [ + "admission_control.cc", + "thread_local_controller.cc", + ], + hdrs = [ + "admission_control.h", + "thread_local_controller.h", + ], + deps = [ + "//include/envoy/http:filter_interface", + "//include/envoy/runtime:runtime_interface", + "//source/common/common:cleanup_lib", + "//source/common/http:codes_lib", + "//source/common/runtime:runtime_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/admission_control/evaluators:response_evaluator_lib", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "@envoy_api//envoy/extensions/filters/http/admission_control/v3alpha:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "unknown", + status = "alpha", + deps = [ + "//include/envoy/registry", + "//source/common/common:enum_to_int", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/admission_control:admission_control_filter_lib", + "//source/extensions/filters/http/admission_control/evaluators:response_evaluator_lib", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/http/admission_control/v3alpha:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/admission_control/admission_control.cc b/source/extensions/filters/http/admission_control/admission_control.cc new file mode 100644 index 0000000000000..796d52e5f955d --- /dev/null +++ b/source/extensions/filters/http/admission_control/admission_control.cc @@ -0,0 +1,128 @@ +#include "extensions/filters/http/admission_control/admission_control.h" + +#include +#include +#include +#include + +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/grpc/status.h" +#include "envoy/http/codes.h" +#include "envoy/runtime/runtime.h" +#include "envoy/server/filter_config.h" + +#include "common/common/cleanup.h" +#include "common/common/enum_to_int.h" +#include "common/grpc/common.h" +#include "common/http/codes.h" +#include "common/http/utility.h" +#include "common/protobuf/utility.h" + +#include "extensions/filters/http/admission_control/evaluators/default_evaluator.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +using GrpcStatus = Grpc::Status::GrpcStatus; + +static constexpr double defaultAggression = 2.0; + +AdmissionControlFilterConfig::AdmissionControlFilterConfig( + const AdmissionControlProto& proto_config, Runtime::Loader& runtime, TimeSource& time_source, + Runtime::RandomGenerator& random, Stats::Scope& scope, ThreadLocal::SlotPtr&& tls, + std::unique_ptr response_evaluator) + : runtime_(runtime), time_source_(time_source), random_(random), scope_(scope), + tls_(std::move(tls)), admission_control_feature_(proto_config.enabled(), runtime_), + aggression_( + proto_config.has_aggression_coefficient() + ? std::make_unique(proto_config.aggression_coefficient(), runtime_) + : nullptr), + response_evaluator_(std::move(response_evaluator)) {} + +double AdmissionControlFilterConfig::aggression() const { + return std::max(1.0, aggression_ ? aggression_->value() : defaultAggression); +} + +AdmissionControlFilter::AdmissionControlFilter(AdmissionControlFilterConfigSharedPtr config, + const std::string& stats_prefix) + : config_(std::move(config)), stats_(generateStats(config_->scope(), stats_prefix)), + deferred_record_failure_([this]() { config_->getController().recordFailure(); }) {} + +Http::FilterHeadersStatus AdmissionControlFilter::decodeHeaders(Http::RequestHeaderMap&, bool) { + // TODO(tonya11en): Ensure we document the fact that healthchecks are ignored. + if (!config_->filterEnabled() || decoder_callbacks_->streamInfo().healthCheck()) { + return Http::FilterHeadersStatus::Continue; + } + + if (shouldRejectRequest()) { + decoder_callbacks_->sendLocalReply(Http::Code::ServiceUnavailable, "", nullptr, absl::nullopt, + "denied by admission control"); + stats_.rq_rejected_.inc(); + return Http::FilterHeadersStatus::StopIteration; + } + + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterHeadersStatus AdmissionControlFilter::encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) { + bool successful_response = false; + if (Grpc::Common::isGrpcResponseHeaders(headers, end_stream)) { + absl::optional grpc_status = Grpc::Common::getGrpcStatus(headers); + + // If the GRPC status isn't found in the headers, it must be found in the trailers. + expect_grpc_status_in_trailer_ = !grpc_status.has_value(); + if (expect_grpc_status_in_trailer_) { + return Http::FilterHeadersStatus::Continue; + } + + const uint32_t status = enumToInt(grpc_status.value()); + successful_response = config_->responseEvalutor().isGrpcSuccess(status); + } else { + // HTTP response. + const uint64_t http_status = Http::Utility::getResponseStatus(headers); + successful_response = config_->responseEvalutor().isHttpSuccess(http_status); + } + + if (successful_response) { + recordSuccess(); + } else { + recordFailure(); + } + + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterTrailersStatus +AdmissionControlFilter::encodeTrailers(Http::ResponseTrailerMap& trailers) { + if (expect_grpc_status_in_trailer_) { + absl::optional grpc_status = Grpc::Common::getGrpcStatus(trailers, false); + + if (grpc_status.has_value() && config_->responseEvalutor().isGrpcSuccess(grpc_status.value())) { + recordSuccess(); + } else { + recordFailure(); + } + } + + return Http::FilterTrailersStatus::Continue; +} + +bool AdmissionControlFilter::shouldRejectRequest() const { + const double total = config_->getController().requestTotalCount(); + const double success = config_->getController().requestSuccessCount(); + const double probability = (total - config_->aggression() * success) / (total + 1); + + // Choosing an accuracy of 4 significant figures for the probability. + static constexpr uint64_t accuracy = 1e4; + auto r = config_->random().random(); + return (accuracy * std::max(probability, 0.0)) > (r % accuracy); +} + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/admission_control.h b/source/extensions/filters/http/admission_control/admission_control.h new file mode 100644 index 0000000000000..724bfda357643 --- /dev/null +++ b/source/extensions/filters/http/admission_control/admission_control.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include + +#include "envoy/common/time.h" +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/http/codes.h" +#include "envoy/http/filter.h" +#include "envoy/runtime/runtime.h" +#include "envoy/server/filter_config.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +#include "common/common/cleanup.h" +#include "common/grpc/common.h" +#include "common/grpc/status.h" +#include "common/http/codes.h" +#include "common/runtime/runtime_protos.h" + +#include "extensions/filters/http/admission_control/evaluators/response_evaluator.h" +#include "extensions/filters/http/admission_control/thread_local_controller.h" +#include "extensions/filters/http/common/pass_through_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +/** + * All stats for the admission control filter. + */ +#define ALL_ADMISSION_CONTROL_STATS(COUNTER) COUNTER(rq_rejected) + +/** + * Wrapper struct for admission control filter stats. @see stats_macros.h + */ +struct AdmissionControlStats { + ALL_ADMISSION_CONTROL_STATS(GENERATE_COUNTER_STRUCT) +}; + +using AdmissionControlProto = + envoy::extensions::filters::http::admission_control::v3alpha::AdmissionControl; + +/** + * Configuration for the admission control filter. + */ +class AdmissionControlFilterConfig { +public: + AdmissionControlFilterConfig(const AdmissionControlProto& proto_config, Runtime::Loader& runtime, + TimeSource& time_source, Runtime::RandomGenerator& random, + Stats::Scope& scope, ThreadLocal::SlotPtr&& tls, + std::unique_ptr response_evaluator); + virtual ~AdmissionControlFilterConfig() = default; + + virtual ThreadLocalController& getController() const { + return tls_->getTyped(); + } + + Runtime::Loader& runtime() const { return runtime_; } + Runtime::RandomGenerator& random() const { return random_; } + bool filterEnabled() const { return admission_control_feature_.enabled(); } + TimeSource& timeSource() const { return time_source_; } + Stats::Scope& scope() const { return scope_; } + double aggression() const; + ResponseEvaluator& responseEvalutor() const { return *response_evaluator_; } + +private: + Runtime::Loader& runtime_; + TimeSource& time_source_; + Runtime::RandomGenerator& random_; + Stats::Scope& scope_; + const ThreadLocal::SlotPtr tls_; + Runtime::FeatureFlag admission_control_feature_; + std::unique_ptr aggression_; + std::unique_ptr response_evaluator_; +}; + +using AdmissionControlFilterConfigSharedPtr = std::shared_ptr; + +/** + * A filter that probabilistically rejects requests based on upstream success-rate. + */ +class AdmissionControlFilter : public Http::PassThroughFilter, + Logger::Loggable { +public: + AdmissionControlFilter(AdmissionControlFilterConfigSharedPtr config, + const std::string& stats_prefix); + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap&, bool) override; + + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) override; + Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap& trailers) override; + +private: + static AdmissionControlStats generateStats(Stats::Scope& scope, const std::string& prefix) { + return {ALL_ADMISSION_CONTROL_STATS(POOL_COUNTER_PREFIX(scope, prefix))}; + } + + bool shouldRejectRequest() const; + + void recordSuccess() { + config_->getController().recordSuccess(); + ASSERT(deferred_record_failure_); + deferred_record_failure_->cancel(); + } + + void recordFailure() { deferred_record_failure_.reset(); } + + AdmissionControlFilterConfigSharedPtr config_; + AdmissionControlStats stats_; + absl::optional deferred_record_failure_; + bool expect_grpc_status_in_trailer_; +}; + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/config.cc b/source/extensions/filters/http/admission_control/config.cc new file mode 100644 index 0000000000000..c3a3ba232c8f6 --- /dev/null +++ b/source/extensions/filters/http/admission_control/config.cc @@ -0,0 +1,64 @@ +#include "extensions/filters/http/admission_control/config.h" + +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/common/enum_to_int.h" + +#include "extensions/filters/http/admission_control/admission_control.h" +#include "extensions/filters/http/admission_control/evaluators/default_evaluator.h" +#include "extensions/filters/http/admission_control/evaluators/response_evaluator.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +static constexpr std::chrono::seconds defaultSamplingWindow{120}; + +Http::FilterFactoryCb AdmissionControlFilterFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::admission_control::v3alpha::AdmissionControl& config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) { + + const std::string prefix = stats_prefix + "admission_control."; + + // Create the thread-local controller. + auto tls = context.threadLocal().allocateSlot(); + auto sampling_window = std::chrono::seconds( + PROTOBUF_GET_MS_OR_DEFAULT(config, sampling_window, 1000 * defaultSamplingWindow.count()) / + 1000); + tls->set( + [sampling_window, &context](Event::Dispatcher&) -> ThreadLocal::ThreadLocalObjectSharedPtr { + return std::make_shared(context.timeSource(), sampling_window); + }); + + std::unique_ptr response_evaluator; + switch (config.evaluation_criteria_case()) { + case AdmissionControlProto::EvaluationCriteriaCase::kDefaultEvalCriteria: + response_evaluator = std::make_unique(config.default_eval_criteria()); + break; + case AdmissionControlProto::EvaluationCriteriaCase::EVALUATION_CRITERIA_NOT_SET: + NOT_REACHED_GCOVR_EXCL_LINE; + } + + AdmissionControlFilterConfigSharedPtr filter_config = + std::make_shared( + config, context.runtime(), context.timeSource(), context.random(), context.scope(), + std::move(tls), std::move(response_evaluator)); + + return [filter_config, prefix](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared(filter_config, prefix)); + }; +} + +/** + * Static registration for the admission_control filter. @see RegisterFactory. + */ +REGISTER_FACTORY(AdmissionControlFilterFactory, + Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/config.h b/source/extensions/filters/http/admission_control/config.h new file mode 100644 index 0000000000000..8abe84eafefcb --- /dev/null +++ b/source/extensions/filters/http/admission_control/config.h @@ -0,0 +1,32 @@ +#pragma once + +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +/** + * Config registration for the adaptive concurrency limit filter. @see NamedHttpFilterConfigFactory. + */ +class AdmissionControlFilterFactory + : public Common::FactoryBase< + envoy::extensions::filters::http::admission_control::v3alpha::AdmissionControl> { +public: + AdmissionControlFilterFactory() : FactoryBase(HttpFilterNames::get().AdmissionControl) {} + + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::admission_control::v3alpha::AdmissionControl& + proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/evaluators/BUILD b/source/extensions/filters/http/admission_control/evaluators/BUILD new file mode 100644 index 0000000000000..29def5202c818 --- /dev/null +++ b/source/extensions/filters/http/admission_control/evaluators/BUILD @@ -0,0 +1,26 @@ +licenses(["notice"]) # Apache 2 + +# HTTP L7 filter that probabilistically rejects requests based on upstream success-rate. + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "response_evaluator_lib", + srcs = ["default_evaluator.cc"], + hdrs = [ + "default_evaluator.h", + "response_evaluator.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//include/envoy/grpc:status", + "//source/common/common:enum_to_int", + "@envoy_api//envoy/extensions/filters/http/admission_control/v3alpha:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/admission_control/evaluators/default_evaluator.cc b/source/extensions/filters/http/admission_control/evaluators/default_evaluator.cc new file mode 100644 index 0000000000000..e1b9f4a686bd7 --- /dev/null +++ b/source/extensions/filters/http/admission_control/evaluators/default_evaluator.cc @@ -0,0 +1,68 @@ +#include "extensions/filters/http/admission_control/evaluators/default_evaluator.h" + +#include + +#include "envoy/grpc/status.h" + +#include "common/common/enum_to_int.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +DefaultResponseEvaluator::DefaultResponseEvaluator( + envoy::extensions::filters::http::admission_control::v3alpha::AdmissionControl:: + DefaultEvaluationCriteria evaluation_criteria) { + // HTTP status. + if (evaluation_criteria.http_status_size() > 0) { + for (const auto& range : evaluation_criteria.http_status()) { + http_success_fns_.emplace_back([range](uint64_t status) { + return (static_cast(range.start()) <= status) && + (status < static_cast(range.end())); + }); + } + } else { + // We default to all 5xx codes as request failures. + http_success_fns_.emplace_back([](uint64_t status) { return status < 500; }); + } + + // GRPC status. + if (evaluation_criteria.grpc_status_size() > 0) { + for (const auto& status : evaluation_criteria.grpc_status()) { + grpc_success_codes_.emplace_back(status); + } + } else { + grpc_success_codes_ = decltype(grpc_success_codes_)({ + enumToInt(Grpc::Status::WellKnownGrpcStatus::Ok), + enumToInt(Grpc::Status::WellKnownGrpcStatus::Canceled), + enumToInt(Grpc::Status::WellKnownGrpcStatus::Unknown), + enumToInt(Grpc::Status::WellKnownGrpcStatus::InvalidArgument), + enumToInt(Grpc::Status::WellKnownGrpcStatus::NotFound), + enumToInt(Grpc::Status::WellKnownGrpcStatus::AlreadyExists), + enumToInt(Grpc::Status::WellKnownGrpcStatus::Unauthenticated), + enumToInt(Grpc::Status::WellKnownGrpcStatus::FailedPrecondition), + enumToInt(Grpc::Status::WellKnownGrpcStatus::OutOfRange), + enumToInt(Grpc::Status::WellKnownGrpcStatus::Unimplemented), + }); + } +} + +bool DefaultResponseEvaluator::isGrpcSuccess(uint32_t status) const { + return std::count(grpc_success_codes_.begin(), grpc_success_codes_.end(), status) > 0; +} + +bool DefaultResponseEvaluator::isHttpSuccess(uint64_t code) const { + for (const auto& fn : http_success_fns_) { + if (!fn(code)) { + return false; + } + } + + return true; +} + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/evaluators/default_evaluator.h b/source/extensions/filters/http/admission_control/evaluators/default_evaluator.h new file mode 100644 index 0000000000000..3e4157a946488 --- /dev/null +++ b/source/extensions/filters/http/admission_control/evaluators/default_evaluator.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.validate.h" + +#include "extensions/filters/http/admission_control/evaluators/response_evaluator.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +class DefaultResponseEvaluator : public ResponseEvaluator { +public: + DefaultResponseEvaluator(envoy::extensions::filters::http::admission_control::v3alpha:: + AdmissionControl::DefaultEvaluationCriteria evaluation_criteria); + // ResponseEvaluator + bool isHttpSuccess(uint64_t code) const override; + bool isGrpcSuccess(uint32_t status) const override; + +private: + std::vector> http_success_fns_; + std::vector grpc_success_codes_; +}; + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/evaluators/response_evaluator.h b/source/extensions/filters/http/admission_control/evaluators/response_evaluator.h new file mode 100644 index 0000000000000..9915014fdede2 --- /dev/null +++ b/source/extensions/filters/http/admission_control/evaluators/response_evaluator.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "envoy/common/pure.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +/** + * Determines of a request was successful based on response headers. + */ +class ResponseEvaluator { +public: + virtual ~ResponseEvaluator() = default; + + /** + * Returns true if the provided HTTP code constitutes a success. + */ + virtual bool isHttpSuccess(uint64_t code) const PURE; + + /** + * Returns true if the provided gRPC status counts constitutes a success. + */ + virtual bool isGrpcSuccess(uint32_t status) const PURE; +}; + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/thread_local_controller.cc b/source/extensions/filters/http/admission_control/thread_local_controller.cc new file mode 100644 index 0000000000000..ca63be56bf2f4 --- /dev/null +++ b/source/extensions/filters/http/admission_control/thread_local_controller.cc @@ -0,0 +1,49 @@ +#include "extensions/filters/http/admission_control/thread_local_controller.h" + +#include + +#include "envoy/common/pure.h" +#include "envoy/common/time.h" +#include "envoy/http/codes.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +static constexpr std::chrono::seconds defaultHistoryGranularity{1}; + +ThreadLocalControllerImpl::ThreadLocalControllerImpl(TimeSource& time_source, + std::chrono::seconds sampling_window) + : time_source_(time_source), sampling_window_(sampling_window) {} + +void ThreadLocalControllerImpl::maybeUpdateHistoricalData() { + // Purge stale samples. + while (!historical_data_.empty() && ageOfOldestSample() >= sampling_window_) { + removeOldestSample(); + } + + // It's possible we purged stale samples from the history and are left with nothing, so it's + // necessary to add an empty entry. We will also need to roll over into a new entry in the + // historical data if we've exceeded the time specified by the granularity. + if (historical_data_.empty() || ageOfNewestSample() >= defaultHistoryGranularity) { + historical_data_.emplace_back(time_source_.monotonicTime(), RequestData()); + } +} + +void ThreadLocalControllerImpl::recordRequest(const bool success) { + maybeUpdateHistoricalData(); + + // The back of the deque will be the most recent samples. + ++historical_data_.back().second.requests; + ++global_data_.requests; + if (success) { + ++historical_data_.back().second.successes; + ++global_data_.successes; + } +} + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/admission_control/thread_local_controller.h b/source/extensions/filters/http/admission_control/thread_local_controller.h new file mode 100644 index 0000000000000..e624ea95ce754 --- /dev/null +++ b/source/extensions/filters/http/admission_control/thread_local_controller.h @@ -0,0 +1,108 @@ +#pragma once + +#include "envoy/common/pure.h" +#include "envoy/common/time.h" +#include "envoy/http/codes.h" +#include "envoy/thread_local/thread_local.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { + +/* + * Thread-local admission controller interface. + */ +class ThreadLocalController { +public: + virtual ~ThreadLocalController() = default; + + // Record success/failure of a request and update the internal state of the controller to reflect + // this. + virtual void recordSuccess() PURE; + virtual void recordFailure() PURE; + + // Returns the current number of recorded requests. + virtual uint32_t requestTotalCount() PURE; + + // Returns the current number of recorded request successes. + virtual uint32_t requestSuccessCount() PURE; +}; + +/** + * Thread-local object to track request counts and successes over a rolling time window. Request + * data for the time window is kept recent via a circular buffer that phases out old request/success + * counts when recording new samples. + * + * This controller is thread-local so that we do not need to take any locks on the sample histories + * to update them, at the cost of decreasing the number of samples. + * + * The look-back window for request samples is accurate up to a hard-coded 1-second granularity. + * TODO (tonya11en): Allow the granularity to be configurable. + */ +class ThreadLocalControllerImpl : public ThreadLocalController, + public ThreadLocal::ThreadLocalObject { +public: + ThreadLocalControllerImpl(TimeSource& time_source, std::chrono::seconds sampling_window); + ~ThreadLocalControllerImpl() override = default; + void recordSuccess() override { recordRequest(true); } + void recordFailure() override { recordRequest(false); } + + uint32_t requestTotalCount() override { + maybeUpdateHistoricalData(); + return global_data_.requests; + } + uint32_t requestSuccessCount() override { + maybeUpdateHistoricalData(); + return global_data_.successes; + } + +private: + struct RequestData { + uint32_t requests{0}; + uint32_t successes{0}; + }; + + void recordRequest(const bool success); + + // Potentially remove any stale samples and record sample aggregates to the historical data. + void maybeUpdateHistoricalData(); + + // Returns the age of the oldest sample in the historical data. + std::chrono::microseconds ageOfOldestSample() const { + ASSERT(!historical_data_.empty()); + using namespace std::chrono; + return duration_cast(time_source_.monotonicTime() - + historical_data_.front().first); + } + + // Returns the age of the newest sample in the historical data. + std::chrono::microseconds ageOfNewestSample() const { + ASSERT(!historical_data_.empty()); + using namespace std::chrono; + return duration_cast(time_source_.monotonicTime() - + historical_data_.back().first); + } + + // Removes the oldest sample in the historical data and reconciles the global data. + void removeOldestSample() { + ASSERT(!historical_data_.empty()); + global_data_.successes -= historical_data_.front().second.successes; + global_data_.requests -= historical_data_.front().second.requests; + historical_data_.pop_front(); + } + + TimeSource& time_source_; + std::deque> historical_data_; + + // Request data aggregated for the whole look-back window. + RequestData global_data_; + + // The rolling time window size. + std::chrono::seconds sampling_window_; +}; + +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index afa9981a75105..b02a92987ce55 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -64,6 +64,8 @@ class HttpFilterNameValues { const std::string Tap = "envoy.filters.http.tap"; // Adaptive concurrency limit filter const std::string AdaptiveConcurrency = "envoy.filters.http.adaptive_concurrency"; + // Admission control filter + const std::string AdmissionControl = "envoy.filters.http.admission_control"; // Original Src Filter const std::string OriginalSrc = "envoy.filters.http.original_src"; // Dynamic forward proxy filter diff --git a/test/extensions/filters/http/admission_control/BUILD b/test/extensions/filters/http/admission_control/BUILD new file mode 100644 index 0000000000000..f4873d94412b2 --- /dev/null +++ b/test/extensions/filters/http/admission_control/BUILD @@ -0,0 +1,70 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_extension_cc_test( + name = "admission_control_filter_test", + srcs = ["admission_control_filter_test.cc"], + extension_name = "envoy.filters.http.admission_control", + deps = [ + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/extensions/filters/http/admission_control:admission_control_filter_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/admission_control/v3alpha:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.filters.http.admission_control", + deps = [ + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/extensions/filters/http/admission_control:admission_control_filter_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/admission_control/v3alpha:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "admission_control_integration_test", + srcs = ["admission_control_integration_test.cc"], + extension_name = "envoy.filters.http.admission_control", + deps = [ + "//source/extensions/filters/http/admission_control:config", + "//test/integration:http_integration_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "admission_controller_test", + srcs = ["controller_test.cc"], + extension_name = "envoy.filters.http.admission_control", + deps = [ + "//source/common/http:headers_lib", + "//source/extensions/filters/http/admission_control:admission_control_filter_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/admission_control/v3alpha:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/admission_control/admission_control_filter_test.cc b/test/extensions/filters/http/admission_control/admission_control_filter_test.cc new file mode 100644 index 0000000000000..ece7979481258 --- /dev/null +++ b/test/extensions/filters/http/admission_control/admission_control_filter_test.cc @@ -0,0 +1,415 @@ +#include + +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.validate.h" + +#include "common/stats/isolated_store_impl.h" + +#include "extensions/filters/http/admission_control/admission_control.h" +#include "extensions/filters/http/admission_control/evaluators/default_evaluator.h" +#include "extensions/filters/http/admission_control/evaluators/response_evaluator.h" +#include "extensions/filters/http/admission_control/thread_local_controller.h" + +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { +namespace { + +class MockThreadLocalController : public ThreadLocal::ThreadLocalObject, + public ThreadLocalController { +public: + MockThreadLocalController() = default; + MOCK_METHOD(uint32_t, requestTotalCount, (), (override)); + MOCK_METHOD(uint32_t, requestSuccessCount, (), (override)); + MOCK_METHOD(void, recordSuccess, (), (override)); + MOCK_METHOD(void, recordFailure, (), (override)); +}; + +class TestConfig : public AdmissionControlFilterConfig { +public: + TestConfig(const AdmissionControlProto& proto_config, Runtime::Loader& runtime, + TimeSource& time_source, Runtime::RandomGenerator& random, Stats::Scope& scope, + ThreadLocal::SlotPtr&& tls, MockThreadLocalController& controller, + std::unique_ptr evaluator) + : AdmissionControlFilterConfig(proto_config, runtime, time_source, random, scope, + std::move(tls), std::move(evaluator)), + controller_(controller) {} + ThreadLocalController& getController() const override { return controller_; } + +private: + MockThreadLocalController& controller_; +}; + +/** + * TODO (tonya11en): If another response evaluator is implemented, the tests should be separated + * from the filter test. + */ +class AdmissionControlTest : public testing::Test { +public: + AdmissionControlTest() = default; + + std::shared_ptr makeConfig(const std::string& yaml) { + AdmissionControlProto proto; + TestUtility::loadFromYamlAndValidate(yaml, proto); + auto tls = context_.threadLocal().allocateSlot(); + auto evaluator = std::make_unique(proto.default_eval_criteria()); + + return std::make_shared(proto, runtime_, time_system_, random_, scope_, + std::move(tls), controller_, std::move(evaluator)); + } + + void setupFilter(std::shared_ptr config) { + filter_ = std::make_shared(config, "test_prefix."); + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + } + + void sampleGrpcRequest(std::string&& grpc_status) { + Http::TestResponseHeaderMapImpl headers{{"content-type", "application/grpc"}, + {"grpc-status", grpc_status}}; + filter_->encodeHeaders(headers, true); + } + + void sampleHttpRequest(std::string&& http_error_code) { + Http::TestResponseHeaderMapImpl headers{{":status", http_error_code}}; + filter_->encodeHeaders(headers, true); + } + + void expectHttpSuccess(std::string&& code) { + Http::RequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + EXPECT_CALL(controller_, recordSuccess()); + sampleHttpRequest(std::move(code)); + } + + void expectHttpFail(std::string&& code) { + Http::RequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + EXPECT_CALL(controller_, recordFailure()); + sampleHttpRequest(std::move(code)); + } + + void expectGrpcSuccess(std::string&& code) { + Http::RequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + EXPECT_CALL(controller_, recordSuccess()); + sampleGrpcRequest(std::move(code)); + } + + void expectGrpcFail(std::string&& code) { + Http::RequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + EXPECT_CALL(controller_, recordFailure()); + sampleGrpcRequest(std::move(code)); + } + +protected: + std::string stats_prefix_{""}; + NiceMock runtime_; + NiceMock context_; + Stats::IsolatedStoreImpl scope_; + Event::SimulatedTimeSystem time_system_; + NiceMock random_; + std::shared_ptr filter_; + NiceMock decoder_callbacks_; + MockThreadLocalController controller_; + const std::string default_yaml_{R"EOF( +enabled: + default_value: true + runtime_key: "foo.enabled" +sampling_window: 10s +aggression_coefficient: + default_value: 1.0 + runtime_key: "foo.aggression" +default_eval_criteria: + http_status: + grpc_status: +)EOF"}; +}; + +// Ensure the filter can be disabled/enabled via runtime. +TEST_F(AdmissionControlTest, FilterRuntimeOverride) { + const std::string yaml = R"EOF( +enabled: + default_value: true + runtime_key: "foo.enabled" +sampling_window: 10s +aggression_coefficient: + default_value: 1.0 + runtime_key: "foo.aggression" +default_eval_criteria: + http_status: + grpc_status: +)EOF"; + + auto config = makeConfig(yaml); + setupFilter(config); + + // "Disable" the filter via runtime. + EXPECT_CALL(runtime_.snapshot_, getBoolean("foo.enabled", true)).WillRepeatedly(Return(false)); + + // The filter is bypassed via runtime. + EXPECT_CALL(controller_, requestTotalCount()).Times(0); + EXPECT_CALL(controller_, requestSuccessCount()).Times(0); + + // We expect no rejections. + Http::RequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); +} + +// Ensure the filter disregards healthcheck traffic. +TEST_F(AdmissionControlTest, DisregardHealthChecks) { + auto config = makeConfig(default_yaml_); + setupFilter(config); + + StreamInfo::MockStreamInfo stream_info; + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillOnce(testing::ReturnRef(stream_info)); + EXPECT_CALL(stream_info, healthCheck()).WillOnce(Return(true)); + + // Fail lots of requests so that we would normally expect a ~100% rejection rate. It should pass + // below since the request is a healthcheck. + EXPECT_CALL(controller_, requestTotalCount()).Times(0); + EXPECT_CALL(controller_, requestSuccessCount()).Times(0); + + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); +} + +// Validate simple behavioral cases. +TEST_F(AdmissionControlTest, FilterBehaviorBasic) { + auto config = makeConfig(default_yaml_); + setupFilter(config); + + // Fail lots of requests so that we can expect a ~100% rejection rate. + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(1000)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(0)); + + // We expect rejections due to the failure rate. + TestUtility::waitForCounterEq(scope_, "test_prefix.rq_rejected", 0, time_system_); + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, true)); + TestUtility::waitForCounterEq(scope_, "test_prefix.rq_rejected", 1, time_system_); + + // Now we pretend as if the historical data has been phased out. + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(0)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(0)); + + // Should continue forwarding since SR has become stale and there's no additional data. This also + // verifies that HTTP 200s are default successes. + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + EXPECT_CALL(controller_, recordSuccess()); + sampleHttpRequest("200"); + + // Fail exactly half of the requests so we get a ~50% rejection rate. + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(1000)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(500)); + + // Random numbers in the range [0,1e4) are considered for the rejection calculation. One request + // should fail and the other should pass. + EXPECT_CALL(random_, random()).WillOnce(Return(5500)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + EXPECT_CALL(controller_, recordFailure()); + sampleHttpRequest("503"); + + EXPECT_CALL(random_, random()).WillOnce(Return(4500)); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, true)); +} + +// Ensure the HTTP error code range configurations are honored. +TEST_F(AdmissionControlTest, HttpErrorCodes) { + const std::string yaml = R"EOF( +default_eval_criteria: + http_status: + - start: 300 + end: 400 + grpc_status: +)EOF"; + + auto config = makeConfig(yaml); + setupFilter(config); + + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(0)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(0)); + + setupFilter(config); + expectHttpSuccess("300"); + + setupFilter(config); + expectHttpSuccess("301"); + + setupFilter(config); + expectHttpSuccess("302"); + + setupFilter(config); + expectHttpFail("200"); + + setupFilter(config); + expectHttpFail("400"); + + setupFilter(config); + expectHttpFail("500"); +} + +// Verify default behavior of the filter. +TEST_F(AdmissionControlTest, DefaultBehaviorTest) { + const std::string yaml = R"EOF( +default_eval_criteria: + http_status: + grpc_status: +)EOF"; + + auto config = makeConfig(yaml); + + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(0)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(0)); + + setupFilter(config); + expectGrpcSuccess("0"); + + // Aborted. + setupFilter(config); + expectGrpcFail("10"); + + // Data loss. + setupFilter(config); + expectGrpcFail("15"); + + // Deadline exceeded. + setupFilter(config); + expectGrpcFail("4"); + + // Internal + setupFilter(config); + expectGrpcFail("13"); + + // Resource exhausted. + setupFilter(config); + expectGrpcFail("8"); + + // Unavailable. + setupFilter(config); + expectGrpcFail("14"); + + setupFilter(config); + expectHttpSuccess("200"); + setupFilter(config); + expectHttpSuccess("201"); + setupFilter(config); + expectHttpSuccess("204"); + setupFilter(config); + expectHttpSuccess("300"); + setupFilter(config); + expectHttpSuccess("301"); + setupFilter(config); + expectHttpSuccess("404"); + + // 500 is a failure by default. + setupFilter(config); + expectHttpFail("500"); +} + +// Ensure that HTTP status codes are not considered when evaluating a GRPC request. +TEST_F(AdmissionControlTest, HttpCodeInfluence) { + const std::string yaml = R"EOF( +default_eval_criteria: + http_status: + grpc_status: + - 7 # PERMISSION_DENIED + - 12 # UNIMPLEMENTED +)EOF"; + + auto config = makeConfig(yaml); + + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(0)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(0)); + + setupFilter(config); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + + // Verify that the HTTP 200 isn't causing this request to pass as a success even though it's an + // unsuccessful GRPC request. + EXPECT_CALL(controller_, recordFailure()); + Http::TestResponseHeaderMapImpl headers{ + {"content-type", "application/grpc"}, {"grpc-status", "0"}, {":status", "200"}}; + filter_->encodeHeaders(headers, true); +} + +// Ensure that HTTP status codes are not considered when evaluating a GRPC request. +TEST_F(AdmissionControlTest, HttpCodeInfluence2) { + const std::string yaml = R"EOF( +default_eval_criteria: + http_status: + - start: 300 + end: 400 + grpc_status: + - 7 # PERMISSION_DENIED + - 12 # UNIMPLEMENTED +)EOF"; + + auto config = makeConfig(yaml); + + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(0)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(0)); + + setupFilter(config); + + // HTTP 2xx is not considered a success, but it's returned for all of the GRPC messages, so let's + // make sure GRPC still gets evaluated correctly. + expectGrpcSuccess("7"); + expectGrpcFail("0"); + + // Verify that the HTTP behaves correctly as well. A code of 200 counts as a failure in the + // config, so let's make sure it actually fails without a GRPC message type. + expectHttpFail("200"); + expectHttpSuccess("301"); +} + +// Check that GRPC error code configurations are honored. +TEST_F(AdmissionControlTest, GrpcErrorCodes) { + const std::string yaml = R"EOF( +default_eval_criteria: + http_status: + grpc_status: + - 7 + - 13 +)EOF"; + + auto config = makeConfig(yaml); + + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_CALL(controller_, requestTotalCount()).WillRepeatedly(Return(0)); + EXPECT_CALL(controller_, requestSuccessCount()).WillRepeatedly(Return(0)); + + setupFilter(config); + expectGrpcFail("0"); + setupFilter(config); + expectGrpcFail("2"); + + setupFilter(config); + expectGrpcSuccess("13"); + setupFilter(config); + expectGrpcSuccess("7"); +} + +} // namespace +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/admission_control/admission_control_integration_test.cc b/test/extensions/filters/http/admission_control/admission_control_integration_test.cc new file mode 100644 index 0000000000000..d5fea15d65dcc --- /dev/null +++ b/test/extensions/filters/http/admission_control/admission_control_integration_test.cc @@ -0,0 +1,171 @@ +#include "common/grpc/common.h" + +#include "test/integration/autonomous_upstream.h" +#include "test/integration/http_integration.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +namespace Envoy { +namespace { + +const std::string ADMISSION_CONTROL_CONFIG = + R"EOF( +name: envoy.filters.http.admission_control +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.admission_control.v3alpha.AdmissionControl + default_eval_criteria: + http_status: + grpc_status: + sampling_window: 120s + aggression_coefficient: + default_value: 1.0 + runtime_key: "foo.aggression" + enabled: + default_value: true + runtime_key: "foo.enabled" +)EOF"; + +class AdmissionControlIntegrationTest : public Event::TestUsingSimulatedTime, + public testing::TestWithParam, + public HttpIntegrationTest { +public: + AdmissionControlIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam(), realTime()) {} + + void SetUp() override {} + + void initialize() override { + config_helper_.addConfigModifier(setEnableDownstreamTrailersHttp1()); + config_helper_.addFilter(ADMISSION_CONTROL_CONFIG); + HttpIntegrationTest::initialize(); + } + +protected: + void verifyGrpcSuccess(IntegrationStreamDecoderPtr response) { + EXPECT_EQ("0", response->trailers()->GrpcStatus()->value().getStringView()); + } + + void verifyHttpSuccess(IntegrationStreamDecoderPtr response) { + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + } + + IntegrationStreamDecoderPtr sendGrpcRequestWithReturnCode(uint64_t code) { + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Set the response headers on the autonomous upstream. + auto headers = std::make_unique(); + headers->setStatus(200); + headers->setContentType("application/grpc"); + + auto trailers = std::make_unique(); + trailers->setGrpcMessage("this is a message"); + trailers->setGrpcStatus(code); + + auto* au = reinterpret_cast(fake_upstreams_.front().get()); + au->setResponseHeaders(std::move(headers)); + au->setResponseTrailers(std::move(trailers)); + + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + response->waitForEndStream(); + codec_client_->close(); + return response; + } + + IntegrationStreamDecoderPtr sendRequestWithReturnCode(std::string&& code) { + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Set the response headers on the autonomous upstream. + auto* au = reinterpret_cast(fake_upstreams_.front().get()); + au->setResponseHeaders(std::make_unique( + Http::TestHeaderMapImpl({{":status", code}}))); + + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + response->waitForEndStream(); + codec_client_->close(); + return response; + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, AdmissionControlIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +TEST_P(AdmissionControlIntegrationTest, HttpTest) { + autonomous_upstream_ = true; + initialize(); + + // Drop the success rate to a very low value. + ENVOY_LOG(info, "dropping success rate"); + for (int i = 0; i < 1000; ++i) { + sendRequestWithReturnCode("500"); + } + + // Measure throttling rate from the admission control filter. + double throttle_count = 0; + double request_count = 0; + ENVOY_LOG(info, "validating throttling rate"); + for (int i = 0; i < 1000; ++i) { + auto response = sendRequestWithReturnCode("500"); + auto rc = response->headers().Status()->value().getStringView(); + if (rc == "503") { + ++throttle_count; + } else { + ASSERT_EQ(rc, "500"); + } + ++request_count; + } + + // Given the current throttling rate formula with an aggression of 1, it should result in a ~98% + // throttling rate. Allowing an error of 3%. + EXPECT_NEAR(throttle_count / request_count, 0.98, 0.03); + + // We now wait for the history to become stale. + timeSystem().advanceTimeWait(std::chrono::seconds(120)); + + // We expect a 100% success rate after waiting. No throttling should occur. + for (int i = 0; i < 100; ++i) { + verifyHttpSuccess(sendRequestWithReturnCode("200")); + } +} + +TEST_P(AdmissionControlIntegrationTest, GrpcTest) { + autonomous_upstream_ = true; + setUpstreamProtocol(FakeHttpConnection::Type::HTTP2); + initialize(); + + // Drop the success rate to a very low value. + for (int i = 0; i < 1000; ++i) { + sendGrpcRequestWithReturnCode(14); + } + + // Measure throttling rate from the admission control filter. + double throttle_count = 0; + double request_count = 0; + for (int i = 0; i < 1000; ++i) { + auto response = sendGrpcRequestWithReturnCode(10); + + // When the filter is throttling, it returns an HTTP code 503 and the GRPC status is unset. + // Otherwise, we expect a GRPC status of "Unknown" as set above. + if (response->headers().Status()->value().getStringView() == "503") { + ++throttle_count; + } else { + auto grpc_status = Grpc::Common::getGrpcStatus(*(response->trailers())); + ASSERT_EQ(grpc_status, Grpc::Status::WellKnownGrpcStatus::Aborted); + } + ++request_count; + } + + // Given the current throttling rate formula with an aggression of 1, it should result in a ~98% + // throttling rate. Allowing an error of 3%. + EXPECT_NEAR(throttle_count / request_count, 0.98, 0.03); + + // We now wait for the history to become stale. + timeSystem().advanceTimeWait(std::chrono::seconds(120)); + + // We expect a 100% success rate after waiting. No throttling should occur. + for (int i = 0; i < 100; ++i) { + verifyGrpcSuccess(sendGrpcRequestWithReturnCode(0)); + } +} + +} // namespace +} // namespace Envoy diff --git a/test/extensions/filters/http/admission_control/config_test.cc b/test/extensions/filters/http/admission_control/config_test.cc new file mode 100644 index 0000000000000..584d172256884 --- /dev/null +++ b/test/extensions/filters/http/admission_control/config_test.cc @@ -0,0 +1,117 @@ +#include + +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.validate.h" + +#include "common/stats/isolated_store_impl.h" + +#include "extensions/filters/http/admission_control/admission_control.h" +#include "extensions/filters/http/admission_control/evaluators/default_evaluator.h" + +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { +namespace { + +class AdmissionControlConfigTest : public testing::Test { +public: + AdmissionControlConfigTest() = default; + + std::shared_ptr makeConfig(const std::string& yaml) { + AdmissionControlProto proto; + TestUtility::loadFromYamlAndValidate(yaml, proto); + auto tls = context_.threadLocal().allocateSlot(); + auto evaluator = std::make_unique(proto.default_eval_criteria()); + tls->set([this](Event::Dispatcher&) -> ThreadLocal::ThreadLocalObjectSharedPtr { + return std::make_shared(time_system_, std::chrono::seconds(10)); + }); + return std::make_shared( + proto, runtime_, time_system_, random_, scope_, std::move(tls), std::move(evaluator)); + } + +protected: + NiceMock runtime_; + NiceMock context_; + Stats::IsolatedStoreImpl scope_; + Event::SimulatedTimeSystem time_system_; + NiceMock random_; +}; + +// Verify the configuration when all fields are set. +TEST_F(AdmissionControlConfigTest, BasicTestAllConfigured) { + const std::string yaml = R"EOF( +enabled: + default_value: false + runtime_key: "foo.enabled" +sampling_window: 1337s +aggression_coefficient: + default_value: 4.2 + runtime_key: "foo.aggression" +default_eval_criteria: + http_status: + grpc_status: +)EOF"; + + auto config = makeConfig(yaml); + + EXPECT_FALSE(config->filterEnabled()); + EXPECT_EQ(4.2, config->aggression()); +} + +// Verify the config defaults when not specified. +TEST_F(AdmissionControlConfigTest, BasicTestMinimumConfigured) { + // Empty config. No fields are required. + AdmissionControlProto proto; + + const std::string yaml = R"EOF( +default_eval_criteria: + http_status: + grpc_status: +)EOF"; + auto config = makeConfig(yaml); + + EXPECT_TRUE(config->filterEnabled()); + EXPECT_EQ(2.0, config->aggression()); +} + +// Ensure runtime fields are honored. +TEST_F(AdmissionControlConfigTest, VerifyRuntime) { + const std::string yaml = R"EOF( +enabled: + default_value: false + runtime_key: "foo.enabled" +sampling_window: 1337s +aggression_coefficient: + default_value: 4.2 + runtime_key: "foo.aggression" +default_eval_criteria: + http_status: + grpc_status: +)EOF"; + + auto config = makeConfig(yaml); + + EXPECT_CALL(runtime_.snapshot_, getBoolean("foo.enabled", false)).WillOnce(Return(true)); + EXPECT_TRUE(config->filterEnabled()); + EXPECT_CALL(runtime_.snapshot_, getDouble("foo.aggression", 4.2)).WillOnce(Return(1.3)); + EXPECT_EQ(1.3, config->aggression()); +} + +} // namespace +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/admission_control/controller_test.cc b/test/extensions/filters/http/admission_control/controller_test.cc new file mode 100644 index 0000000000000..8d82315cab9da --- /dev/null +++ b/test/extensions/filters/http/admission_control/controller_test.cc @@ -0,0 +1,114 @@ +#include + +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.h" +#include "envoy/extensions/filters/http/admission_control/v3alpha/admission_control.pb.validate.h" + +#include "extensions/filters/http/admission_control/thread_local_controller.h" + +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AdmissionControl { +namespace { + +class ThreadLocalControllerTest : public testing::Test { +public: + ThreadLocalControllerTest() : window_(5), tlc_(time_system_, window_) {} + +protected: + // Submit a single request per entry in the historical data (this comes out to a single request + // each second). The final sample does not advance time to allow for testing of this transition. + void fillHistorySlots(const bool successes = true) { + std::function record; + if (successes) { + record = [this]() { tlc_.recordSuccess(); }; + } else { + record = [this]() { tlc_.recordFailure(); }; + } + for (int tick = 0; tick < window_.count(); ++tick) { + record(); + time_system_.advanceTimeWait(std::chrono::seconds(1)); + } + // Don't sleep after the final sample to allow for measurements. + record(); + } + + Event::SimulatedTimeSystem time_system_; + std::chrono::seconds window_; + ThreadLocalControllerImpl tlc_; +}; + +// Test the basic functionality of the admission controller. +TEST_F(ThreadLocalControllerTest, BasicRecord) { + EXPECT_EQ(0, tlc_.requestTotalCount()); + EXPECT_EQ(0, tlc_.requestSuccessCount()); + + tlc_.recordFailure(); + EXPECT_EQ(1, tlc_.requestTotalCount()); + EXPECT_EQ(0, tlc_.requestSuccessCount()); + + tlc_.recordSuccess(); + EXPECT_EQ(2, tlc_.requestTotalCount()); + EXPECT_EQ(1, tlc_.requestSuccessCount()); +} + +// Verify that stale historical samples are removed when they grow stale. +TEST_F(ThreadLocalControllerTest, RemoveStaleSamples) { + fillHistorySlots(); + + // We expect a single request counted in each second of the window. + EXPECT_EQ(window_.count(), tlc_.requestTotalCount()); + EXPECT_EQ(window_.count(), tlc_.requestSuccessCount()); + + time_system_.advanceTimeWait(std::chrono::seconds(1)); + + // Continuing to sample requests at 1 per second should maintain the same request counts. We'll + // record failures here. + fillHistorySlots(false); + EXPECT_EQ(window_.count(), tlc_.requestTotalCount()); + EXPECT_EQ(0, tlc_.requestSuccessCount()); + + // Expect the oldest entry to go stale. + time_system_.advanceTimeWait(std::chrono::seconds(1)); + EXPECT_EQ(window_.count() - 1, tlc_.requestTotalCount()); + EXPECT_EQ(0, tlc_.requestSuccessCount()); +} + +// Verify that stale historical samples are removed when they grow stale. +TEST_F(ThreadLocalControllerTest, RemoveStaleSamples2) { + fillHistorySlots(); + + // We expect a single request counted in each second of the window. + EXPECT_EQ(window_.count(), tlc_.requestTotalCount()); + EXPECT_EQ(window_.count(), tlc_.requestSuccessCount()); + + // Let's just sit here for a full day. We expect all samples to become stale. + time_system_.advanceTimeWait(std::chrono::hours(24)); + + EXPECT_EQ(0, tlc_.requestTotalCount()); + EXPECT_EQ(0, tlc_.requestSuccessCount()); +} + +// Verify that historical samples are made only when there is data to record. +TEST_F(ThreadLocalControllerTest, VerifyMemoryUsage) { + // Make sure we don't add any null data to the history if there are sparse requests. + tlc_.recordSuccess(); + time_system_.advanceTimeWait(std::chrono::seconds(1)); + tlc_.recordSuccess(); + time_system_.advanceTimeWait(std::chrono::seconds(3)); + tlc_.recordSuccess(); + EXPECT_EQ(3, tlc_.requestTotalCount()); + EXPECT_EQ(3, tlc_.requestSuccessCount()); +} + +} // namespace +} // namespace AdmissionControl +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/integration/autonomous_upstream.cc b/test/integration/autonomous_upstream.cc index 649dbe2430296..72a72256d1bc8 100644 --- a/test/integration/autonomous_upstream.cc +++ b/test/integration/autonomous_upstream.cc @@ -59,7 +59,8 @@ void AutonomousStream::sendResponse() { HeaderToInt(RESPONSE_SIZE_BYTES, response_body_length, headers); encodeHeaders(upstream_.responseHeaders(), false); - encodeData(response_body_length, true); + encodeData(response_body_length, false); + encodeTrailers(upstream_.responseTrailers()); } AutonomousHttpConnection::AutonomousHttpConnection(AutonomousUpstream& autonomous_upstream, @@ -110,12 +111,24 @@ std::unique_ptr AutonomousUpstream::lastRequestH return std::move(last_request_headers_); } +void AutonomousUpstream::setResponseTrailers( + std::unique_ptr&& response_trailers) { + Thread::LockGuard lock(headers_lock_); + response_trailers_ = std::move(response_trailers); +} + void AutonomousUpstream::setResponseHeaders( std::unique_ptr&& response_headers) { Thread::LockGuard lock(headers_lock_); response_headers_ = std::move(response_headers); } +Http::TestResponseTrailerMapImpl AutonomousUpstream::responseTrailers() { + Thread::LockGuard lock(headers_lock_); + Http::TestResponseTrailerMapImpl return_trailers = *response_trailers_; + return return_trailers; +} + Http::TestHeaderMapImpl AutonomousUpstream::responseHeaders() { Thread::LockGuard lock(headers_lock_); Http::TestHeaderMapImpl return_headers = *response_headers_; diff --git a/test/integration/autonomous_upstream.h b/test/integration/autonomous_upstream.h index 5abb7bc186be0..aa6c7bfa593e4 100644 --- a/test/integration/autonomous_upstream.h +++ b/test/integration/autonomous_upstream.h @@ -56,6 +56,7 @@ class AutonomousUpstream : public FakeUpstream { bool allow_incomplete_streams) : FakeUpstream(address, type, time_system), allow_incomplete_streams_(allow_incomplete_streams), + response_trailers_(std::make_unique()), response_headers_(std::make_unique( Http::TestHeaderMapImpl({{":status", "200"}}))) {} @@ -64,6 +65,7 @@ class AutonomousUpstream : public FakeUpstream { Event::TestTimeSystem& time_system, bool allow_incomplete_streams) : FakeUpstream(std::move(transport_socket_factory), port, type, version, time_system), allow_incomplete_streams_(allow_incomplete_streams), + response_trailers_(std::make_unique()), response_headers_(std::make_unique( Http::TestHeaderMapImpl({{":status", "200"}}))) {} @@ -77,13 +79,16 @@ class AutonomousUpstream : public FakeUpstream { void setLastRequestHeaders(const Http::HeaderMap& headers); std::unique_ptr lastRequestHeaders(); + void setResponseTrailers(std::unique_ptr&& response_trailers); void setResponseHeaders(std::unique_ptr&& response_headers); + Http::TestResponseTrailerMapImpl responseTrailers(); Http::TestHeaderMapImpl responseHeaders(); const bool allow_incomplete_streams_{false}; private: Thread::MutexBasicLockable headers_lock_; std::unique_ptr last_request_headers_; + std::unique_ptr response_trailers_; std::unique_ptr response_headers_; std::vector http_connections_; std::vector shared_connections_; diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 00bbda734aa32..22aeeff740b55 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -565,6 +565,7 @@ epoll errno etag etags +evaluator evbuffer evbuffers evconnlistener @@ -859,6 +860,7 @@ preorder prepend prepended prev +probabilistically proc profiler programmatically