Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2
api_proto_package(
deps = [
"//envoy/config/core/v3:pkg",
"//envoy/config/route/v3:pkg",
"//envoy/type/v3:pkg",
"@com_github_cncf_xds//udpa/annotations:pkg",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ syntax = "proto3";
package envoy.extensions.filters.http.admission_control.v3;

import "envoy/config/core/v3/base.proto";
import "envoy/config/route/v3/route_components.proto";
import "envoy/type/v3/range.proto";

import "google/protobuf/duration.proto";
Expand All @@ -19,7 +20,52 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// [#protodoc-title: Admission Control]
// [#extension: envoy.filters.http.admission_control]

// Configuration for a single admission control rule. Each rule specifies matching conditions
// and admission control parameters for requests that match those conditions.
// [#next-free-field: 8]
message AdmissionControlRule {
// Specifies a set of headers that the filter should match on. The admission control filter
// will only be applied to requests that match all the specified headers. If not specified,
// the rule will match all requests.
repeated config.route.v3.HeaderMatcher headers = 1;

// Defines how a request is considered a success/failure.
oneof evaluation_criteria {
option (validate.required) = true;

AdmissionControl.SuccessCriteria success_criteria = 2;
}

// The sliding time window over which the success rate is calculated. The window is rounded to the
// nearest second. Defaults to 30s.
google.protobuf.Duration sampling_window = 3;

// Rejection probability is defined by the formula::
//
// max(0, (rq_count - rq_success_count / sr_threshold) / (rq_count + 1)) ^ (1 / aggression)
//
// The aggression dictates how heavily the admission controller will throttle requests upon SR
// dropping at or below the threshold. A value of 1 will result in a linear increase in
// rejection probability as SR drops. Any values less than 1.0, will be set to 1.0. If the
// message is unspecified, the aggression is 1.0.
config.core.v3.RuntimeDouble aggression = 4;

// Dictates the success rate at which the rejection probability is non-zero. As success rate drops
// below this threshold, rejection probability will increase. Any success rate above the threshold
// results in a rejection probability of 0. Defaults to 95%.
config.core.v3.RuntimePercent sr_threshold = 5;

// If the average RPS of the sampling window is below this threshold, the request
// will not be rejected, even if the success rate is lower than sr_threshold.
// Defaults to 0.
config.core.v3.RuntimeUInt32 rps_threshold = 6;

// The probability of rejection will never exceed this value, even if the failure rate is rising.
// Defaults to 80%.
config.core.v3.RuntimePercent max_rejection_probability = 7;
}

// [#next-free-field: 10]
message AdmissionControl {
// Default method of specifying what constitutes a successful request. All status codes that
// indicate a successful request must be explicitly specified if not relying on the default
Expand Down Expand Up @@ -100,4 +146,10 @@ message AdmissionControl {
// The probability of rejection will never exceed this value, even if the failure rate is rising.
// Defaults to 80%.
config.core.v3.RuntimePercent max_rejection_probability = 7;

// Specifies a list of admission control rules. Each rule can have its own matching conditions and
// admission control parameters. Rules are evaluated in order, and the first matching rule will be
// applied. If no rules match (or if ``rules`` is empty), the filter will fall back to the global
// configuration (fields 2–8).
repeated AdmissionControlRule rules = 9;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@

#include "source/common/common/cleanup.h"
#include "source/common/common/enum_to_int.h"
#include "source/common/common/fmt.h"
#include "source/common/grpc/common.h"
#include "source/common/http/codes.h"
#include "source/common/http/header_utility.h"
#include "source/common/http/utility.h"
#include "source/common/protobuf/utility.h"
#include "source/extensions/filters/http/admission_control/evaluators/success_criteria_evaluator.h"
Expand All @@ -32,64 +34,110 @@ static constexpr double defaultSuccessRateThreshold = 95.0;
static constexpr uint32_t defaultRpsThreshold = 0;
static constexpr double defaultMaxRejectionProbability = 80.0;

AdmissionControlFilterConfig::AdmissionControlFilterConfig(
const AdmissionControlProto& proto_config, Runtime::Loader& runtime,
Random::RandomGenerator& random, Stats::Scope& scope,
AdmissionControlRuleConfig::AdmissionControlRuleConfig(
const AdmissionControlRuleProto& rule_config, Runtime::Loader& runtime,
ThreadLocal::TypedSlotPtr<ThreadLocalControllerImpl>&& tls,
std::shared_ptr<ResponseEvaluator> response_evaluator)
: random_(random), scope_(scope), tls_(std::move(tls)),
admission_control_feature_(proto_config.enabled(), runtime),
aggression_(proto_config.has_aggression()
? std::make_unique<Runtime::Double>(proto_config.aggression(), runtime)
std::shared_ptr<ResponseEvaluator> response_evaluator,
std::vector<Http::HeaderUtility::HeaderDataPtr>&& filter_headers)
: filter_headers_(std::move(filter_headers)), tls_(std::move(tls)),
aggression_(rule_config.has_aggression()
? std::make_unique<Runtime::Double>(rule_config.aggression(), runtime)
: nullptr),
sr_threshold_(proto_config.has_sr_threshold() ? std::make_unique<Runtime::Percentage>(
proto_config.sr_threshold(), runtime)
: nullptr),
rps_threshold_(proto_config.has_rps_threshold()
? std::make_unique<Runtime::UInt32>(proto_config.rps_threshold(), runtime)
sr_threshold_(rule_config.has_sr_threshold()
? std::make_unique<Runtime::Percentage>(rule_config.sr_threshold(), runtime)
: nullptr),
rps_threshold_(rule_config.has_rps_threshold()
? std::make_unique<Runtime::UInt32>(rule_config.rps_threshold(), runtime)
: nullptr),
max_rejection_probability_(proto_config.has_max_rejection_probability()
max_rejection_probability_(rule_config.has_max_rejection_probability()
? std::make_unique<Runtime::Percentage>(
proto_config.max_rejection_probability(), runtime)
rule_config.max_rejection_probability(), runtime)
: nullptr),
response_evaluator_(std::move(response_evaluator)) {}

double AdmissionControlFilterConfig::aggression() const {
AdmissionControlRuleConfig::AdmissionControlRuleConfig(
ThreadLocal::TypedSlotPtr<ThreadLocalControllerImpl>&& tls,
std::shared_ptr<ResponseEvaluator> response_evaluator,
std::vector<Http::HeaderUtility::HeaderDataPtr>&& filter_headers,
std::unique_ptr<Runtime::Double>&& aggression,
std::unique_ptr<Runtime::Percentage>&& sr_threshold,
std::unique_ptr<Runtime::UInt32>&& rps_threshold,
std::unique_ptr<Runtime::Percentage>&& max_rejection_probability)
: filter_headers_(std::move(filter_headers)), tls_(std::move(tls)),
aggression_(std::move(aggression)), sr_threshold_(std::move(sr_threshold)),
rps_threshold_(std::move(rps_threshold)),
max_rejection_probability_(std::move(max_rejection_probability)),
response_evaluator_(std::move(response_evaluator)) {}

double AdmissionControlRuleConfig::aggression() const {
return std::max<double>(1.0, aggression_ ? aggression_->value() : defaultAggression);
}

double AdmissionControlFilterConfig::successRateThreshold() const {
double AdmissionControlRuleConfig::successRateThreshold() const {
const double pct = sr_threshold_ ? sr_threshold_->value() : defaultSuccessRateThreshold;
return std::min<double>(pct, 100.0) / 100.0;
}

uint32_t AdmissionControlFilterConfig::rpsThreshold() const {
uint32_t AdmissionControlRuleConfig::rpsThreshold() const {
return rps_threshold_ ? rps_threshold_->value() : defaultRpsThreshold;
}

double AdmissionControlFilterConfig::maxRejectionProbability() const {
double AdmissionControlRuleConfig::maxRejectionProbability() const {
const double ret = max_rejection_probability_ ? max_rejection_probability_->value()
: defaultMaxRejectionProbability;
return ret / 100.0;
}

AdmissionControlFilterConfig::AdmissionControlFilterConfig(
const AdmissionControlProto& proto_config, Runtime::Loader& runtime,
Random::RandomGenerator& random, Stats::Scope& scope,
AdmissionControlRuleConfigSharedPtr&& global,
std::vector<AdmissionControlRuleConfigSharedPtr>&& rules)
: random_(random), scope_(scope), admission_control_feature_(proto_config.enabled(), runtime),
global_(std::move(global)), rules_(std::move(rules)) {}

const AdmissionControlRuleConfig*
AdmissionControlFilterConfig::findMatchingRule(const Http::RequestHeaderMap& headers) const {
if (!rules_.empty()) {
for (const auto& rule : rules_) {
if (rule->filterHeaders().empty() ||
Http::HeaderUtility::matchHeaders(headers, rule->filterHeaders())) {
return rule.get();
}
}
return global_.get();
}
return global_.get();
}

AdmissionControlFilter::AdmissionControlFilter(AdmissionControlFilterConfigSharedPtr config,
const std::string& stats_prefix)
: config_(std::move(config)), stats_(generateStats(config_->scope(), stats_prefix)) {}

Http::FilterHeadersStatus AdmissionControlFilter::decodeHeaders(Http::RequestHeaderMap&, bool) {
Http::FilterHeadersStatus AdmissionControlFilter::decodeHeaders(Http::RequestHeaderMap& headers,
bool) {
if (!config_->filterEnabled() || decoder_callbacks_->streamInfo().healthCheck()) {
// We must forego recording the success/failure of this request during encoding.
record_request_ = false;
return Http::FilterHeadersStatus::Continue;
}

if (config_->getController().averageRps() < config_->rpsThreshold()) {
ENVOY_LOG(debug, "Current rps: {} is below rps_threshold: {}, continue");
matched_rule_config_ = config_->findMatchingRule(headers);
if (!matched_rule_config_) {
record_request_ = false;
return Http::FilterHeadersStatus::Continue;
}

uint32_t rps_threshold = matched_rule_config_->rpsThreshold();
ThreadLocalController& controller = matched_rule_config_->getController();

if (controller.averageRps() < rps_threshold) {
ENVOY_LOG(debug, "Current rps: {} is below rps_threshold: {}, continue",
controller.averageRps(), rps_threshold);
return Http::FilterHeadersStatus::Continue;
}

if (shouldRejectRequest()) {
if (shouldRejectRequest(matched_rule_config_)) {
// We do not want to sample requests that we are rejecting, since this taints the measurements
// that should be describing the upstreams. In addition, if we were to record the requests
// rejected, the rejection probabilities would not converge back to 0 even if the upstream
Expand All @@ -114,6 +162,8 @@ Http::FilterHeadersStatus AdmissionControlFilter::encodeHeaders(Http::ResponseHe
return Http::FilterHeadersStatus::Continue;
}

ResponseEvaluator& evaluator = matched_rule_config_->responseEvaluator();

bool successful_response = false;
if (Grpc::Common::isGrpcResponseHeaders(headers, end_stream)) {
absl::optional<GrpcStatus> grpc_status = Grpc::Common::getGrpcStatus(headers);
Expand All @@ -125,17 +175,17 @@ Http::FilterHeadersStatus AdmissionControlFilter::encodeHeaders(Http::ResponseHe
}

const uint32_t status = enumToInt(grpc_status.value());
successful_response = config_->responseEvaluator().isGrpcSuccess(status);
successful_response = evaluator.isGrpcSuccess(status);
} else {
// HTTP response.
const uint64_t http_status = Http::Utility::getResponseStatus(headers);
successful_response = config_->responseEvaluator().isHttpSuccess(http_status);
successful_response = evaluator.isHttpSuccess(http_status);
}

if (successful_response) {
recordSuccess();
recordSuccess(matched_rule_config_);
} else {
recordFailure();
recordFailure(matched_rule_config_);
}

return Http::FilterHeadersStatus::Continue;
Expand All @@ -144,32 +194,37 @@ Http::FilterHeadersStatus AdmissionControlFilter::encodeHeaders(Http::ResponseHe
Http::FilterTrailersStatus
AdmissionControlFilter::encodeTrailers(Http::ResponseTrailerMap& trailers) {
if (expect_grpc_status_in_trailer_) {
ResponseEvaluator& evaluator = matched_rule_config_->responseEvaluator();

absl::optional<GrpcStatus> grpc_status = Grpc::Common::getGrpcStatus(trailers, false);

if (grpc_status.has_value() &&
config_->responseEvaluator().isGrpcSuccess(grpc_status.value())) {
recordSuccess();
if (grpc_status.has_value() && evaluator.isGrpcSuccess(grpc_status.value())) {
recordSuccess(matched_rule_config_);
} else {
recordFailure();
recordFailure(matched_rule_config_);
}
}

return Http::FilterTrailersStatus::Continue;
}

bool AdmissionControlFilter::shouldRejectRequest() const {
bool AdmissionControlFilter::shouldRejectRequest(
const AdmissionControlRuleConfig* rule_config) const {
// This formula is documented in the admission control filter documentation:
// https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/admission_control_filter.html
const auto request_counts = config_->getController().requestCounts();
ThreadLocalController& controller = rule_config->getController();
const auto request_counts = controller.requestCounts();
const double total_requests = request_counts.requests;
const double successful_requests = request_counts.successes;
double probability = total_requests - successful_requests / config_->successRateThreshold();
const double sr_threshold = rule_config->successRateThreshold();
double probability = total_requests - successful_requests / sr_threshold;
probability = probability / (total_requests + 1);
const auto aggression = config_->aggression();
const auto aggression = rule_config->aggression();
if (aggression != 1.0) {
probability = std::pow(probability, 1.0 / aggression);
}
probability = std::min<double>(probability, config_->maxRejectionProbability());
const double max_rejection_probability = rule_config->maxRejectionProbability();
probability = std::min<double>(probability, max_rejection_probability);

// Choosing an accuracy of 4 significant figures for the probability.
static constexpr uint64_t accuracy = 1e4;
Expand Down
Loading
Loading