Skip to content

Commit

Permalink
route match: Add runtime_fraction field for more granular routing (en…
Browse files Browse the repository at this point in the history
…voyproxy#4560)

This patch reintroduces PR envoyproxy#4217.

Signed-off-by: Tony Allen <tallen@lyft.com>
Signed-off-by: Aaltan Ahmad <aa@stripe.com>
  • Loading branch information
Tony Allen authored and aa-stripe committed Oct 11, 2018
1 parent e34714e commit fa5d9a2
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 41 deletions.
3 changes: 3 additions & 0 deletions DEPRECATED.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ A logged warning is expected for each deprecated item that is in deprecation win
* Setting hosts via `hosts` field in `Cluster` is deprecated. Use `load_assignment` instead.
* Use of `response_headers_to_*` and `request_headers_to_add` are deprecated at the `RouteAction`
level. Please use the configuration options at the `Route` level.
* Use of `runtime` in `RouteMatch`, found in
[route.proto](https://github.com/envoyproxy/envoy/blob/master/api/envoy/api/v2/route/route.proto).
Set the `runtime_fraction` field instead.
* Use of the string `user` field in `Authenticated` in [rbac.proto](https://github.com/envoyproxy/envoy/blob/master/api/envoy/config/rbac/v2alpha/rbac.proto)
is deprecated in favor of the new `StringMatcher` based `principal_name` field.

Expand Down
4 changes: 4 additions & 0 deletions api/envoy/api/v2/core/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ api_proto_library_internal(
visibility = [
":friends",
],
deps = [
"//envoy/type:percent",
],
)

api_go_proto_library(
name = "base",
proto = ":base",
deps = ["//envoy/type:percent_go_proto"],
)

api_proto_library_internal(
Expand Down
12 changes: 12 additions & 0 deletions api/envoy/api/v2/core/base.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import "google/protobuf/wrappers.proto";
import "validate/validate.proto";
import "gogoproto/gogo.proto";

import "envoy/type/percent.proto";

option (gogoproto.equal_all) = true;

// [#protodoc-title: Common types]
Expand Down Expand Up @@ -216,3 +218,13 @@ message SocketOption {
SocketState state = 6
[(validate.rules).message.required = true, (validate.rules).enum.defined_only = true];
}

// Runtime derived FractionalPercent with defaults for when the numerator or denominator is not
// specified via a runtime key.
message RuntimeFractionalPercent {
// Default value if the runtime value's for the numerator/denominator keys are not available.
envoy.type.FractionalPercent default_value = 1 [(validate.rules).message.required = true];

// Runtime key for a YAML representation of a FractionalPercent.
string runtime_key = 2;
}
37 changes: 27 additions & 10 deletions api/envoy/api/v2/route/route.proto
Original file line number Diff line number Diff line change
Expand Up @@ -291,16 +291,33 @@ message RouteMatch {
// is true.
google.protobuf.BoolValue case_sensitive = 4;

// Indicates that the route should additionally match on a runtime key. An
// integer between 0-100. Every time the route is considered for a match, a
// random number between 0-99 is selected. If the number is <= the value found
// in the key (checked first) or, if the key is not present, the default
// value, the route is a match (assuming everything also about the route
// matches). A runtime route configuration can be used to roll out route changes in a
// gradual manner without full code/config deploys. Refer to the
// :ref:`traffic shifting <config_http_conn_man_route_table_traffic_splitting_shift>` docs
// for additional documentation.
core.RuntimeUInt32 runtime = 5;
oneof runtime_specifier {
// Indicates that the route should additionally match on a runtime key. An integer between
// 0-100. Every time the route is considered for a match, a random number between 0-99 is
// selected. If the number is <= the value found in the key (checked first) or, if the key is
// not present, the default value, the route is a match (assuming everything also about the
// route matches). A runtime route configuration can be used to roll out route changes in a
// gradual manner without full code/config deploys. Refer to the :ref:`traffic shifting
// <config_http_conn_man_route_table_traffic_splitting_shift>` docs for additional
// documentation.
//
// .. attention::
//
// **This field is deprecated**. Set the
// :ref:`runtime_fraction<envoy_api_field_route.RouteMatch.runtime_fraction>` field instead.
core.RuntimeUInt32 runtime = 5 [deprecated = true];

// Indicates that the route should additionally match on a runtime key. Every time the route
// is considered for a match, it must also fall under the percentage of matches indicated by
// this field. For some fraction N/D, a random number in the range [0,D) is selected. If the
// number is <= the value of the numberator N, or if the key is not present, the default
// value, the router continues to evaluate the remaining match criteria. A runtime_fraction
// route configuration can be used to roll out route changes in a gradual manner (with more
// granularity than the deprecated runtime field) without full code/config deploys. Refer to
// the :ref:`traffic shifting <config_http_conn_man_route_table_traffic_splitting_shift>` docs
// for additional documentation.
core.RuntimeFractionalPercent runtime_fraction = 9;
}

// Specifies a set of headers that the route should match on. The router will
// check the request’s headers against all the specified headers in the route
Expand Down
7 changes: 4 additions & 3 deletions source/common/access_log/access_log_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ bool RuntimeFilter::evaluate(const RequestInfo::RequestInfo&,
const Http::HeaderEntry* uuid = request_header.RequestId();
uint64_t random_value;
if (use_independent_randomness_ || uuid == nullptr ||
!UuidUtils::uuidModBy(uuid->value().c_str(), random_value,
ProtobufPercentHelper::fractionalPercentDenominatorToInt(percent_))) {
!UuidUtils::uuidModBy(
uuid->value().c_str(), random_value,
ProtobufPercentHelper::fractionalPercentDenominatorToInt(percent_.denominator()))) {
random_value = random_.random();
}

return runtime_.snapshot().featureEnabled(
runtime_key_, percent_.numerator(), random_value,
ProtobufPercentHelper::fractionalPercentDenominatorToInt(percent_));
ProtobufPercentHelper::fractionalPercentDenominatorToInt(percent_.denominator()));
}

OperatorFilter::OperatorFilter(const Protobuf::RepeatedPtrField<
Expand Down
5 changes: 3 additions & 2 deletions source/common/protobuf/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ uint64_t convertPercent(double percent, uint64_t max_value) {
return max_value * (percent / 100.0);
}

uint64_t fractionalPercentDenominatorToInt(const envoy::type::FractionalPercent& percent) {
switch (percent.denominator()) {
uint64_t fractionalPercentDenominatorToInt(
const envoy::type::FractionalPercent::DenominatorType& denominator) {
switch (denominator) {
case envoy::type::FractionalPercent::HUNDRED:
return 100;
case envoy::type::FractionalPercent::TEN_THOUSAND:
Expand Down
5 changes: 3 additions & 2 deletions source/common/protobuf/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ uint64_t convertPercent(double percent, uint64_t max_value);

/**
* Convert a fractional percent denominator enum into an integer.
* @param percent supplies percent to convert.
* @param denominator supplies denominator to convert.
* @return the converted denominator.
*/
uint64_t fractionalPercentDenominatorToInt(const envoy::type::FractionalPercent& percent);
uint64_t fractionalPercentDenominatorToInt(
const envoy::type::FractionalPercent::DenominatorType& denominator);

} // namespace ProtobufPercentHelper
} // namespace Envoy
Expand Down
46 changes: 37 additions & 9 deletions source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@

#include "envoy/http/header_map.h"
#include "envoy/runtime/runtime.h"
#include "envoy/type/percent.pb.validate.h"
#include "envoy/upstream/cluster_manager.h"
#include "envoy/upstream/upstream.h"

#include "common/common/assert.h"
#include "common/common/empty_string.h"
#include "common/common/fmt.h"
#include "common/common/hash.h"
#include "common/common/logger.h"
#include "common/common/utility.h"
#include "common/config/metadata.h"
#include "common/config/rds_json.h"
Expand All @@ -26,6 +28,7 @@
#include "common/http/headers.h"
#include "common/http/utility.h"
#include "common/http/websocket/ws_handler_impl.h"
#include "common/protobuf/protobuf.h"
#include "common/protobuf/utility.h"
#include "common/router/retry_state_impl.h"

Expand Down Expand Up @@ -258,7 +261,7 @@ RouteEntryImplBase::RouteEntryImplBase(const VirtualHostImpl& vhost,
timeout_(PROTOBUF_GET_MS_OR_DEFAULT(route.route(), timeout, DEFAULT_ROUTE_TIMEOUT_MS)),
idle_timeout_(PROTOBUF_GET_OPTIONAL_MS(route.route(), idle_timeout)),
max_grpc_timeout_(PROTOBUF_GET_OPTIONAL_MS(route.route(), max_grpc_timeout)),
runtime_(loadRuntimeData(route.match())), loader_(factory_context.runtime()),
loader_(factory_context.runtime()), runtime_(loadRuntimeData(route.match())),
host_redirect_(route.redirect().host_redirect()),
path_redirect_(route.redirect().path_redirect()),
https_redirect_(route.redirect().https_redirect()),
Expand Down Expand Up @@ -346,8 +349,11 @@ bool RouteEntryImplBase::matchRoute(const Http::HeaderMap& headers, uint64_t ran
bool matches = true;

if (runtime_) {
matches &= loader_.snapshot().featureEnabled(runtime_.value().key_, runtime_.value().default_,
random_value);
matches &= random_value % runtime_->denominator_val_ < runtime_->numerator_val_;
if (!matches) {
// No need to waste further cycles calculating a route match.
return false;
}
}

if (match_grpc_) {
Expand Down Expand Up @@ -409,14 +415,36 @@ void RouteEntryImplBase::finalizeResponseHeaders(
absl::optional<RouteEntryImplBase::RuntimeData>
RouteEntryImplBase::loadRuntimeData(const envoy::api::v2::route::RouteMatch& route_match) {
absl::optional<RuntimeData> runtime;
if (route_match.has_runtime()) {
RuntimeData data;
data.key_ = route_match.runtime().runtime_key();
data.default_ = route_match.runtime().default_value();
runtime = data;
RuntimeData runtime_data;

if (route_match.runtime_specifier_case() == envoy::api::v2::route::RouteMatch::kRuntimeFraction) {
envoy::type::FractionalPercent fractional_percent;
const std::string& fraction_yaml =
loader_.snapshot().get(route_match.runtime_fraction().runtime_key());

try {
MessageUtil::loadFromYamlAndValidate(fraction_yaml, fractional_percent);
} catch (const EnvoyException& ex) {
ENVOY_LOG(error, "failed to parse string value for runtime key {}: {}",
route_match.runtime_fraction().runtime_key(), ex.what());
fractional_percent = route_match.runtime_fraction().default_value();
}

runtime_data.numerator_val_ = fractional_percent.numerator();
runtime_data.denominator_val_ =
ProtobufPercentHelper::fractionalPercentDenominatorToInt(fractional_percent.denominator());
} else if (route_match.runtime_specifier_case() == envoy::api::v2::route::RouteMatch::kRuntime) {
// For backwards compatibility, the deprecated 'runtime' field must be converted to a
// RuntimeData format with a variable denominator type. The 'runtime' field assumes a percentage
// (0-100), so the hard-coded denominator value reflects this.
runtime_data.denominator_val_ = 100;
runtime_data.numerator_val_ = loader_.snapshot().getInteger(
route_match.runtime().runtime_key(), route_match.runtime().default_value());
} else {
return runtime;
}

return runtime;
return runtime_data;
}

void RouteEntryImplBase::finalizePathHeader(Http::HeaderMap& headers,
Expand Down
12 changes: 6 additions & 6 deletions source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ class RouteEntryImplBase : public RouteEntry,
public DirectResponseEntry,
public Route,
public PathMatchCriterion,
public std::enable_shared_from_this<RouteEntryImplBase> {
public std::enable_shared_from_this<RouteEntryImplBase>,
Logger::Loggable<Logger::Id::router> {
public:
/**
* @throw EnvoyException with reason if the route configuration contains any errors
Expand Down Expand Up @@ -386,8 +387,8 @@ class RouteEntryImplBase : public RouteEntry,

private:
struct RuntimeData {
std::string key_{};
uint64_t default_{};
uint64_t numerator_val_{};
uint64_t denominator_val_{};
};

class DynamicRouteEntry : public RouteEntry, public Route {
Expand Down Expand Up @@ -518,8 +519,7 @@ class RouteEntryImplBase : public RouteEntry,

typedef std::shared_ptr<WeightedClusterEntry> WeightedClusterEntrySharedPtr;

static absl::optional<RuntimeData>
loadRuntimeData(const envoy::api::v2::route::RouteMatch& route);
absl::optional<RuntimeData> loadRuntimeData(const envoy::api::v2::route::RouteMatch& route);

static std::multimap<std::string, std::string>
parseOpaqueConfig(const envoy::api::v2::route::Route& route);
Expand All @@ -540,8 +540,8 @@ class RouteEntryImplBase : public RouteEntry,
const std::chrono::milliseconds timeout_;
const absl::optional<std::chrono::milliseconds> idle_timeout_;
const absl::optional<std::chrono::milliseconds> max_grpc_timeout_;
const absl::optional<RuntimeData> runtime_;
Runtime::Loader& loader_;
const absl::optional<RuntimeData> runtime_;
const std::string host_redirect_;
const std::string path_redirect_;
const bool https_redirect_;
Expand Down
10 changes: 6 additions & 4 deletions source/extensions/filters/http/fault/fault_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,14 @@ bool FaultFilter::isDelayEnabled() {
bool enabled = config_->runtime().snapshot().featureEnabled(
DELAY_PERCENT_KEY, fault_settings_->delayPercentage().numerator(),
config_->randomGenerator().random(),
ProtobufPercentHelper::fractionalPercentDenominatorToInt(fault_settings_->delayPercentage()));
ProtobufPercentHelper::fractionalPercentDenominatorToInt(
fault_settings_->delayPercentage().denominator()));
if (!downstream_cluster_delay_percent_key_.empty()) {
enabled |= config_->runtime().snapshot().featureEnabled(
downstream_cluster_delay_percent_key_, fault_settings_->delayPercentage().numerator(),
config_->randomGenerator().random(),
ProtobufPercentHelper::fractionalPercentDenominatorToInt(
fault_settings_->delayPercentage()));
fault_settings_->delayPercentage().denominator()));
}
return enabled;
}
Expand All @@ -149,13 +150,14 @@ bool FaultFilter::isAbortEnabled() {
bool enabled = config_->runtime().snapshot().featureEnabled(
ABORT_PERCENT_KEY, fault_settings_->abortPercentage().numerator(),
config_->randomGenerator().random(),
ProtobufPercentHelper::fractionalPercentDenominatorToInt(fault_settings_->abortPercentage()));
ProtobufPercentHelper::fractionalPercentDenominatorToInt(
fault_settings_->abortPercentage().denominator()));
if (!downstream_cluster_abort_percent_key_.empty()) {
enabled |= config_->runtime().snapshot().featureEnabled(
downstream_cluster_abort_percent_key_, fault_settings_->abortPercentage().numerator(),
config_->randomGenerator().random(),
ProtobufPercentHelper::fractionalPercentDenominatorToInt(
fault_settings_->abortPercentage()));
fault_settings_->abortPercentage().denominator()));
}
return enabled;
}
Expand Down
2 changes: 1 addition & 1 deletion source/extensions/filters/network/mongo_proxy/proxy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ absl::optional<uint64_t> ProxyFilter::delayDuration() {
fault_config_->delayPercentage().numerator(),
generator_.random(),
ProtobufPercentHelper::fractionalPercentDenominatorToInt(
fault_config_->delayPercentage()))) {
fault_config_->delayPercentage().denominator()))) {
return result;
}

Expand Down
Loading

0 comments on commit fa5d9a2

Please sign in to comment.