Skip to content
Merged
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,5 @@ extensions/filters/http/oauth2 @rgs1 @derekargueta @snowp
/*/extensions/filters/http/kill_request @qqustc @htuch
# Rate limit expression descriptor
/*/extensions/rate_limit_descriptors/expr @kyessenov @lizan
# hash input matcher
/*/extensions/matching/input_matchers/consistent_hashing @snowp @donyu

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ proto_library(
"//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg",
"//envoy/extensions/internal_redirect/previous_routes/v3:pkg",
"//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg",
"//envoy/extensions/matching/input_matchers/consistent_hashing/v3:pkg",
"//envoy/extensions/network/socket_interface/v3:pkg",
"//envoy/extensions/rate_limit_descriptors/expr/v3:pkg",
"//envoy/extensions/retry/host/omit_host_metadata/v3:pkg",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
syntax = "proto3";

package envoy.extensions.matching.input_matchers.consistent_hashing.v3;

import "udpa/annotations/migrate.proto";
import "udpa/annotations/status.proto";
import "udpa/annotations/versioning.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.matching.input_matchers.consistent_hashing.v3";
option java_outer_classname = "ConsistentHashingProto";
option java_multiple_files = true;
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Consistent Hashing Matcher]
// [#extension: envoy.matching.input_matchers.consistent_hashing]

// The consistent hashing matchers computes a consistent hash from the input and matches if the resulting hash
// is within the configured threshold.
// More specifically, this matcher evaluates to true if hash(input) % modulo > threshold.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: >=

message ConsistentHashing {
// The threshold the resulting hash must be over in order for this matcher to evaluate to true.
// This value must be below the configured modulo value.
uint32 threshold = 1 [(validate.rules).uint32 = {gt: 0}];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be zero to mean 100%?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point, will remove the validation


// The value to use for the modulus in the calculation. This effectively bounds the hash output,
// specifying the range of possible values.
// This value must be above the configured threshold.
uint32 modulo = 2 [(validate.rules).uint32 = {gt: 0}];
}
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ proto_library(
"//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg",
"//envoy/extensions/internal_redirect/previous_routes/v3:pkg",
"//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg",
"//envoy/extensions/matching/input_matchers/consistent_hashing/v3:pkg",
"//envoy/extensions/network/socket_interface/v3:pkg",
"//envoy/extensions/rate_limit_descriptors/expr/v3:pkg",
"//envoy/extensions/retry/host/omit_host_metadata/v3:pkg",
Expand Down
1 change: 1 addition & 0 deletions generated_api_shadow/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ proto_library(
"//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg",
"//envoy/extensions/internal_redirect/previous_routes/v3:pkg",
"//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg",
"//envoy/extensions/matching/input_matchers/consistent_hashing/v3:pkg",
"//envoy/extensions/network/socket_interface/v3:pkg",
"//envoy/extensions/rate_limit_descriptors/expr/v3:pkg",
"//envoy/extensions/retry/host/omit_host_metadata/v3:pkg",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion include/envoy/matcher/matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
#include "absl/types/optional.h"

namespace Envoy {

namespace Server {
namespace Configuration {
class FactoryContext;
}
} // namespace Server

namespace Matcher {

// This file describes a MatchTree<DataType>, which traverses a tree of matches until it
Expand Down Expand Up @@ -143,7 +150,9 @@ using InputMatcherPtr = std::unique_ptr<InputMatcher>;
*/
class InputMatcherFactory : public Config::TypedFactory {
public:
virtual InputMatcherPtr createInputMatcher(const Protobuf::Message& config) PURE;
virtual InputMatcherPtr
createInputMatcher(const Protobuf::Message& config,
Server::Configuration::FactoryContext& factory_context) PURE;

std::string category() const override { return "envoy.matching.input_matcher"; }
};
Expand Down
17 changes: 9 additions & 8 deletions source/common/matcher/matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ static inline MaybeMatchResult evaluateMatch(MatchTree<DataType>& match_tree,
*/
template <class DataType> class MatchTreeFactory {
public:
MatchTreeFactory(ProtobufMessage::ValidationVisitor& validation_visitor)
: validation_visitor_(validation_visitor) {}
MatchTreeFactory(Server::Configuration::FactoryContext& factory_context)
: factory_context_(factory_context) {}

MatchTreeSharedPtr<DataType> create(const envoy::config::common::matcher::v3::Matcher& config) {
switch (config.matcher_type_case()) {
Expand Down Expand Up @@ -148,7 +148,7 @@ template <class DataType> class MatchTreeFactory {
} else if (on_match.has_action()) {
auto& factory = Config::Utility::getAndCheckFactory<ActionFactory>(on_match.action());
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
on_match.action().typed_config(), validation_visitor_, factory);
on_match.action().typed_config(), factory_context_.messageValidationVisitor(), factory);
return OnMatch<DataType>{factory.createActionFactoryCb(*message), {}};
}

Expand All @@ -159,8 +159,8 @@ template <class DataType> class MatchTreeFactory {
createDataInput(const envoy::config::core::v3::TypedExtensionConfig& config) {
auto& factory = Config::Utility::getAndCheckFactory<DataInputFactory<DataType>>(config);
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
config.typed_config(), validation_visitor_, factory);
return factory.createDataInput(*message, validation_visitor_);
config.typed_config(), factory_context_.messageValidationVisitor(), factory);
return factory.createDataInput(*message, factory_context_.messageValidationVisitor());
}

InputMatcherPtr createInputMatcher(
Expand All @@ -175,15 +175,16 @@ template <class DataType> class MatchTreeFactory {
auto& factory =
Config::Utility::getAndCheckFactory<InputMatcherFactory>(predicate.custom_match());
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
predicate.custom_match().typed_config(), validation_visitor_, factory);
return factory.createInputMatcher(*message);
predicate.custom_match().typed_config(), factory_context_.messageValidationVisitor(),
factory);
return factory.createInputMatcher(*message, factory_context_);
}
default:
NOT_REACHED_GCOVR_EXCL_LINE;
}
}

ProtobufMessage::ValidationVisitor& validation_visitor_;
Server::Configuration::FactoryContext& factory_context_;
};
} // namespace Matcher
} // namespace Envoy
6 changes: 6 additions & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ EXTENSIONS = {

"envoy.health_checkers.redis": "//source/extensions/health_checkers/redis:config",

#
# Input Matchers
#

"envoy.matching.input_matchers.consistent_hashing": "//source/extensions/matching/input_matchers/consistent_hashing:config",

#
# HTTP filters
#
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_extension",
"envoy_cc_library",
"envoy_extension_package",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_library(
name = "consistent_hashing_lib",
hdrs = ["matcher.h"],
deps = [
"//include/envoy/upstream:retry_interface",
"//source/common/common:hash_lib",
],
)

envoy_cc_extension(
name = "config",
srcs = ["config.cc"],
hdrs = ["config.h"],
security_posture = "robust_to_untrusted_downstream",
deps = [
":consistent_hashing_lib",
"//include/envoy/matcher:matcher_interface",
"//include/envoy/registry",
"//include/envoy/server:factory_context_interface",
"@envoy_api//envoy/extensions/matching/input_matchers/consistent_hashing/v3:pkg_cc_proto",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include "extensions/matching/input_matchers/consistent_hashing/config.h"

namespace Envoy {
namespace Extensions {
namespace Matching {
namespace InputMatchers {
namespace ConsistentHashing {

Envoy::Matcher::InputMatcherPtr ConsistentHashingConfig::createInputMatcher(
const Protobuf::Message& config, Server::Configuration::FactoryContext& factory_context) {
const auto& consistent_hashing_config =
MessageUtil::downcastAndValidate<const envoy::extensions::matching::input_matchers::
consistent_hashing::v3::ConsistentHashing&>(
config, factory_context.messageValidationVisitor());

if (consistent_hashing_config.threshold() > consistent_hashing_config.modulo()) {
throw EnvoyException(fmt::format("threshold cannot be greater than modulo: {} > {}",
consistent_hashing_config.threshold(),
consistent_hashing_config.modulo()));
}

return std::make_unique<Matcher>(consistent_hashing_config.threshold(),
consistent_hashing_config.modulo());
}
/**
* Static registration for the consistent hashing matcher. @see RegisterFactory.
*/
REGISTER_FACTORY(ConsistentHashingConfig, Envoy::Matcher::InputMatcherFactory);

} // namespace ConsistentHashing
} // namespace InputMatchers
} // namespace Matching
} // namespace Extensions
} // namespace Envoy
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include "envoy/extensions/matching/input_matchers/consistent_hashing/v3/consistent_hashing.pb.h"
#include "envoy/extensions/matching/input_matchers/consistent_hashing/v3/consistent_hashing.pb.validate.h"
#include "envoy/matcher/matcher.h"
#include "envoy/server/factory_context.h"

#include "common/protobuf/utility.h"

#include "extensions/matching/input_matchers/consistent_hashing/matcher.h"

namespace Envoy {
namespace Extensions {
namespace Matching {
namespace InputMatchers {
namespace ConsistentHashing {

class ConsistentHashingConfig : public Envoy::Matcher::InputMatcherFactory {
public:
Envoy::Matcher::InputMatcherPtr
createInputMatcher(const Protobuf::Message& config,
Server::Configuration::FactoryContext& factory_context) override;

std::string name() const override { return "envoy.matching.matchers.consistent_hashing"; }

ProtobufTypes::MessagePtr createEmptyConfigProto() override {
return std::make_unique<
envoy::extensions::matching::input_matchers::consistent_hashing::v3::ConsistentHashing>();
}
};
} // namespace ConsistentHashing
} // namespace InputMatchers
} // namespace Matching
} // namespace Extensions
} // namespace Envoy
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once

#include "envoy/matcher/matcher.h"

#include "common/common/hash.h"

namespace Envoy {
namespace Extensions {
namespace Matching {
namespace InputMatchers {
namespace ConsistentHashing {

class Matcher : public Envoy::Matcher::InputMatcher {
public:
Matcher(uint32_t threshold, uint32_t modulo) : threshold_(threshold), modulo_(modulo) {}
bool match(absl::optional<absl::string_view> input) override {
// Only match if the value is present.
if (!input) {
return false;
}

// Otherwise, match if (hash(input) % modulo) > threshold.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: >=

return HashUtil::xxHash64(*input) % modulo_ > threshold_;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One interesting implication of the consistent hasher API guarantee that the results are fleet-wide consistent is that if we ever change xxHash64, or xxhash itself internally makes some inconsistent breaking change, the property will be violated during rollouts. FWIW we have this problem today with our affinity load balancers. Not sure how you prefer to handle it (maybe warn in API documentation is enough?) but worth considering.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a warning to the docs, not sure what else we could do here besides implementing our own hash that we promise we'll never change (or never update xxhash?).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we model 100% match here given the >? Should it be >=?

}

private:
const uint32_t threshold_;
const uint32_t modulo_;
};
} // namespace ConsistentHashing
} // namespace InputMatchers
} // namespace Matching
} // namespace Extensions
} // namespace Envoy
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_package",
)
load(
"//test/extensions:extensions_build_system.bzl",
"envoy_extension_cc_test",
)

licenses(["notice"]) # Apache 2

envoy_package()

envoy_extension_cc_test(
name = "config_test",
srcs = ["config_test.cc"],
extension_name = "envoy.matching.input_matchers.consistent_hashing",
deps = [
"//source/extensions/matching/input_matchers/consistent_hashing:config",
"//test/mocks/server:factory_context_mocks",
],
)

envoy_extension_cc_test(
name = "matcher_test",
srcs = ["matcher_test.cc"],
extension_name = "envoy.matching.input_matchers.consistent_hashing",
deps = [
"//source/extensions/matching/input_matchers/consistent_hashing:consistent_hashing_lib",
],
)
Loading