Skip to content

Matcher + MatcherTree support for a keep_matching field in OnMatch #38726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
07a8bb3
Add a new keep_matching field to OnMatch to mark a Matcher whose acti…
bsurber Mar 12, 2025
7d66365
Update Envoy's MatchTree interface to support keep_matching by adding…
bsurber Mar 12, 2025
f973da6
Properly propagate keep_matching in OnMatch factory callbacks & catch…
bsurber Mar 13, 2025
4ec98f3
Formatting fixes
bsurber Mar 13, 2025
296ce9b
DONOTSUBMIT: temporarily use the matcher proto change from my xds for…
bsurber Mar 13, 2025
322a3e7
Merge remote-tracking branch 'origin/main' into keep-matching-matchers
bsurber Apr 10, 2025
d85941c
Add testing to exercise both local & xds definitions of the Matcher p…
bsurber Apr 10, 2025
52b1ee1
Adjust matcher_test to improve readability with new macros
bsurber Apr 10, 2025
28474f1
Update new empty() checks to IsEmpty() expectations in matcher_test.cc
bsurber Apr 10, 2025
952187f
Add reentrants to the spelling check dictionary
bsurber Apr 11, 2025
e63e666
Adjust raw string spacing to cleanup the PR diff
bsurber Apr 11, 2025
4a57495
Centralize keep_matching & sub-matcher recursion handling in evaluate…
bsurber May 1, 2025
6e1e190
ListMatcherReentrant update to directly refer to original ListMatcher…
bsurber Apr 28, 2025
9639b2c
Merge remote-tracking branch 'upstream/main' into keep-matching-matchers
bsurber Apr 28, 2025
c82b721
Update new match_delegate test's OnMatch initializations for keep_mat…
bsurber Apr 29, 2025
ed4542d
style-fix: ListMatcherReentrant raw ptr changed to reference
bsurber Apr 29, 2025
258ab56
Merge remote-tracking branch 'upstream/main' into keep-matching-matchers
bsurber Apr 30, 2025
4804b2e
Update the default behavior of keep_matching. If set in a context tha…
bsurber Apr 30, 2025
e7e187e
Refactor MatcherAmbiguousTest to simplify matcher creation across tests
bsurber May 1, 2025
9b24cf1
Add clarification of default behaviors when the field's support hasn'…
bsurber May 1, 2025
7f543fb
Add TODO comment for ravenblackx to implement a reentrant for prefix_…
bsurber May 1, 2025
374515b
Update cncf/xds dependency back to using the main repo now that the m…
bsurber May 2, 2025
b7a7a2a
Update stringaction-vector matcher checking in matcher_test
bsurber May 2, 2025
9055263
Nit: small list-matcher iteration change
bsurber May 5, 2025
20bdcb0
Merge remote-tracking branch 'upstream/main' into keep-matching-matchers
bsurber May 6, 2025
f05db5e
Update OnMatch validation logic to not throw an exception & instead f…
bsurber May 7, 2025
a99199b
Cleanup unused ignore_multiple_skipped_results_ field that is unneede…
bsurber May 8, 2025
5b8f246
Add coverage to sub-matcher testing for when a sub-matcher reentrant …
bsurber May 8, 2025
18be96f
Add coverage for sub-matchers that return failure responses. This als…
bsurber May 9, 2025
1a8d14b
Move keep_matching and recursion handling back into each MatchTree im…
bsurber May 16, 2025
a3e01f0
minor fixes
bsurber May 16, 2025
a1b9864
Merge remote-tracking branch 'upstream/main' into keep-matching-matchers
bsurber May 19, 2025
dc8631d
Nit fix for keep_matching_ initialization
bsurber May 20, 2025
b1c09b2
Nit fix for skipped match callbacks
bsurber May 22, 2025
acb80ac
Update API comment per markdroth
ravenblackx May 28, 2025
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
6 changes: 3 additions & 3 deletions api/bazel/repository_locations.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ REPOSITORY_LOCATIONS_SPEC = dict(
project_desc = "xDS API Working Group (xDS-WG)",
project_url = "https://github.com/cncf/xds",
# During the UDPA -> xDS migration, we aren't working with releases.
version = "b4127c9b8d78b77423fd25169f05b7476b6ea932",
sha256 = "aa5f1596bbef3f277dcf4700e4c1097b34301ae66f3b79cd731e3adfbaff2f8f",
release_date = "2024-09-05",
version = "2ac532fd44436293585084f8d94c6bdb17835af0",
sha256 = "790c4c83b6950bb602fec221f6a529d9f368cdc8852aae7d2592d0d04b015f37",
release_date = "2025-05-01",
strip_prefix = "xds-{version}",
urls = ["https://github.com/cncf/xds/archive/{version}.tar.gz"],
use_category = ["api"],
Expand Down
11 changes: 11 additions & 0 deletions api/envoy/config/common/matcher/v3/matcher.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ message Matcher {
// Protocol-specific action to take.
core.v3.TypedExtensionConfig action = 2;
}

// If true, the action will be taken but the caller will behave as if no
// match was found. This applies both to actions directly encoded in the
// action field and to actions returned from a nested matcher tree in the
// matcher field. A subsequent matcher on_no_match action will be used
// instead.
//
// This field is not supported in all contexts in which the matcher API is
// used. If this field is set in a context in which it's not supported,
// the resource will be rejected.
bool keep_matching = 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

This PR implements this field only for list matchers, but there's nothing here in the API indicating that this field doesn't work for all matcher types, which may lead to confusion. I think we should either document the fact that this currently works only for list matchers and then update that as we implement it for each new matcher type, or we should just include the implementation for all matcher types in this PR so that there's no issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

Still need to address this comment.

Same thing in cncf/xds#117.

Copy link
Contributor Author

@bsurber bsurber May 1, 2025

Choose a reason for hiding this comment

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

The keep_matching field now has an updated comment for behavior without supporting implementation.

Support has been implemented for ListMatcher and TrieMatcher (ended up needing it for 63439a3), and a TODO has been left for ravenblackx in PrefixMapMatcher.

}

// A linear list of field matchers.
Expand Down
36 changes: 35 additions & 1 deletion envoy/matcher/matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ template <class ActionFactoryContext> class ActionFactory : public Config::Typed
template <class DataType> struct OnMatch {
const ActionFactoryCb action_cb_;
const MatchTreeSharedPtr<DataType> matcher_;
bool keep_matching_{};
};
template <class DataType> using OnMatchFactoryCb = std::function<OnMatch<DataType>()>;

Expand Down Expand Up @@ -138,6 +139,8 @@ enum class MatchState {
MatchComplete,
};

// Callback to execute against skipped matches' actions.
template <class DataType> using SkippedMatchCb = std::function<void(const OnMatch<DataType>&)>;
/**
* MatchTree provides the interface for performing matches against the data provided by DataType.
*/
Expand All @@ -160,7 +163,38 @@ template <class DataType> class MatchTree {
// matching requirements). If the match couldn't be completed, {false, {}} will be returned.
// If a match result was determined, {true, action} will be returned. If a match result was
// determined to be no match, {true, {}} will be returned.
virtual MatchResult match(const DataType& matching_data) PURE;
virtual MatchResult match(const DataType& matching_data,
SkippedMatchCb<DataType> skipped_match_cb = nullptr) PURE;

protected:
// Internally handle recursion & keep_matching logic in matcher implementations.
// This should be called against initial matching & on-no-match results.
static inline MatchResult
handleRecursionAndSkips(const absl::optional<OnMatch<DataType>>& on_match, const DataType& data,
SkippedMatchCb<DataType> skipped_match_cb) {
if (!on_match.has_value()) {
return {MatchState::MatchComplete, absl::nullopt};
}
if (on_match->matcher_) {
auto nested_result = on_match->matcher_->match(data, skipped_match_cb);
// Parent result's keep_matching skips the nested result.
if (on_match->keep_matching_ && nested_result.match_state_ == MatchState::MatchComplete &&
nested_result.on_match_.has_value()) {
if (skipped_match_cb) {
skipped_match_cb(*nested_result.on_match_);
}
return {MatchState::MatchComplete, absl::nullopt};
}
return nested_result;
}
if (on_match->action_cb_ && on_match->keep_matching_) {
if (skipped_match_cb) {
skipped_match_cb(*on_match);
}
return {MatchState::MatchComplete, absl::nullopt};
}
return {MatchState::MatchComplete, on_match};
}
};

template <class DataType> using MatchTreeSharedPtr = std::shared_ptr<MatchTree<DataType>>;
Expand Down
27 changes: 20 additions & 7 deletions source/common/matcher/list_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,34 @@ template <class DataType> class ListMatcher : public MatchTree<DataType> {
public:
explicit ListMatcher(absl::optional<OnMatch<DataType>> on_no_match) : on_no_match_(on_no_match) {}

typename MatchTree<DataType>::MatchResult match(const DataType& matching_data) override {
using MatchResult = typename MatchTree<DataType>::MatchResult;

typename MatchTree<DataType>::MatchResult
match(const DataType& matching_data,
SkippedMatchCb<DataType> skipped_match_cb = nullptr) override {
for (const auto& matcher : matchers_) {
const auto maybe_match = matcher.first->match(matching_data);
FieldMatchResult result = matcher.first->match(matching_data);

// One of the matchers don't have enough information, bail on evaluating the match.
if (maybe_match.match_state_ == MatchState::UnableToMatch) {
return {MatchState::UnableToMatch, {}};
if (result.match_state_ == MatchState::UnableToMatch) {
return {MatchState::UnableToMatch, absl::nullopt};
}
if (!result.result()) {
continue;
}

if (maybe_match.result()) {
return {MatchState::MatchComplete, matcher.second};
MatchResult processed_result = MatchTree<DataType>::handleRecursionAndSkips(
matcher.second, matching_data, skipped_match_cb);
// Continue to next matcher if the result is a no-match or is skipped.
if (processed_result.match_state_ != MatchState::MatchComplete ||
processed_result.on_match_.has_value()) {
return processed_result;
}
}

return {MatchState::MatchComplete, on_no_match_};
// Return on-no-match, after keep_matching and/or recursion handling.
return MatchTree<DataType>::handleRecursionAndSkips(on_no_match_, matching_data,
skipped_match_cb);
}

void addMatcher(FieldMatcherPtr<DataType>&& matcher, OnMatch<DataType> action) {
Expand Down
37 changes: 22 additions & 15 deletions source/common/matcher/map_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class MapMatcher : public MatchTree<DataType>, Logger::Loggable<Logger::Id::matc
// Adds a child to the map.
virtual void addChild(std::string value, OnMatch<DataType>&& on_match) PURE;

typename MatchTree<DataType>::MatchResult match(const DataType& data) override {
typename MatchTree<DataType>::MatchResult
match(const DataType& data, SkippedMatchCb<DataType> skipped_match_cb = nullptr) override {
const auto input = data_input_->get(data);
ENVOY_LOG(trace, "Attempting to match {}", input);
if (input.data_availability_ == DataInputGetResult::DataAvailability::NotAvailable) {
Expand All @@ -28,27 +29,33 @@ class MapMatcher : public MatchTree<DataType>, Logger::Loggable<Logger::Id::matc

// Returns `on_no_match` when input data is empty. (i.e., is absl::monostate).
if (absl::holds_alternative<absl::monostate>(input.data_)) {
return {MatchState::MatchComplete, on_no_match_};
return MatchTree<DataType>::handleRecursionAndSkips(on_no_match_, data, skipped_match_cb);
}

const auto result = doMatch(absl::get<std::string>(input.data_));
if (result) {
if (result->matcher_) {
return result->matcher_->match(data);
} else {
return {MatchState::MatchComplete, OnMatch<DataType>{result->action_cb_, nullptr}};
const absl::optional<OnMatch<DataType>> result = doMatch(absl::get<std::string>(input.data_));
// No match.
if (!result.has_value()) {
// Match failed.
if (input.data_availability_ ==
DataInputGetResult::DataAvailability::MoreDataMightBeAvailable) {
return {MatchState::UnableToMatch, absl::nullopt};
}
} else if (input.data_availability_ ==
DataInputGetResult::DataAvailability::MoreDataMightBeAvailable) {
// It's possible that we were attempting a lookup with a partial value, so delay matching
// until we know that we actually failed.
return {MatchState::UnableToMatch, absl::nullopt};
// No-match, return on_no_match after keep_matching and/or recursion handling.
return MatchTree<DataType>::handleRecursionAndSkips(on_no_match_, data, skipped_match_cb);
}

return {MatchState::MatchComplete, on_no_match_};
// Handle recursion and keep_matching.
auto processed_result =
MatchTree<DataType>::handleRecursionAndSkips(result, data, skipped_match_cb);
// Matched or failed nested matching.
if (processed_result.match_state_ != MatchState::MatchComplete ||
processed_result.on_match_.has_value()) {
return processed_result;
}
// No-match, return on_no_match after keep_matching and/or recursion handling.
return MatchTree<DataType>::handleRecursionAndSkips(on_no_match_, data, skipped_match_cb);
}

protected:
template <class DataType2, class ActionFactoryContext> friend class MatchTreeFactory;
MapMatcher(DataInputPtr<DataType>&& data_input, absl::optional<OnMatch<DataType>> on_no_match,
absl::Status& creation_status)
Expand Down
37 changes: 23 additions & 14 deletions source/common/matcher/matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,20 @@ struct MaybeMatchResult {

// TODO(snowp): Make this a class that tracks the progress to speed up subsequent traversals.
template <class DataType>
static inline MaybeMatchResult evaluateMatch(MatchTree<DataType>& match_tree,
const DataType& data) {
const auto result = match_tree.match(data);
static inline MaybeMatchResult evaluateMatch(MatchTree<DataType>& match_tree, const DataType& data,
SkippedMatchCb<DataType> skipped_match_cb = nullptr) {
const auto result = match_tree.match(data, skipped_match_cb);
if (result.match_state_ == MatchState::UnableToMatch) {
return MaybeMatchResult{nullptr, MatchState::UnableToMatch};
return {nullptr, MatchState::UnableToMatch};
}

if (!result.on_match_) {
return {nullptr, MatchState::MatchComplete};
}

if (result.on_match_->matcher_) {
return evaluateMatch(*result.on_match_->matcher_, data);
}

return MaybeMatchResult{result.on_match_->action_cb_, MatchState::MatchComplete};
// Note: does not handle sub-matchers or keep_matching, MatchTree::match(...) implementations are
// expected to do so.
return {result.on_match_->action_cb_, MatchState::MatchComplete};
}

template <class DataType> using FieldMatcherFactoryCb = std::function<FieldMatcherPtr<DataType>()>;
Expand All @@ -73,8 +71,9 @@ template <class DataType> class AnyMatcher : public MatchTree<DataType> {
explicit AnyMatcher(absl::optional<OnMatch<DataType>> on_no_match)
: on_no_match_(std::move(on_no_match)) {}

typename MatchTree<DataType>::MatchResult match(const DataType&) override {
return {MatchState::MatchComplete, on_no_match_};
typename MatchTree<DataType>::MatchResult
match(const DataType& data, SkippedMatchCb<DataType> skipped_match_cb = nullptr) override {
return MatchTree<DataType>::handleRecursionAndSkips(on_no_match_, data, skipped_match_cb);
}
const absl::optional<OnMatch<DataType>> on_no_match_;
};
Expand Down Expand Up @@ -153,6 +152,7 @@ class MatchTreeFactory : public OnMatchFactory<DataType> {
Server::Configuration::ServerFactoryContext& factory_context,
MatchTreeValidationVisitor<DataType>& validation_visitor)
: action_factory_context_(context), server_factory_context_(factory_context),
on_match_validation_visitor_(validation_visitor),
match_input_factory_(factory_context.messageValidationVisitor(), validation_visitor) {}

// TODO(snowp): Remove this type parameter once we only have one Matcher proto.
Expand Down Expand Up @@ -324,9 +324,15 @@ class MatchTreeFactory : public OnMatchFactory<DataType> {

template <class OnMatchType>
absl::optional<OnMatchFactoryCb<DataType>> createOnMatchBase(const OnMatchType& on_match) {
on_match_validation_visitor_.validateOnMatch(on_match);
if (const std::vector<absl::Status>& errors = on_match_validation_visitor_.errors();
!errors.empty()) {
return []() -> OnMatch<DataType> { return OnMatch<DataType>{}; };
}
Comment on lines +327 to +331
Copy link
Member

Choose a reason for hiding this comment

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

I think we should reject this configuration if the keep_on_matching is not supported on this context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In practice, it is rejected just by adding to the errors vector. The match tree's validator is checked by callers. There is a slight laziness in that I'm returning a lambda instead of absl::nullopt, but that's because none of the various callers to createOnMatchBase(...) actually check for has_value() atm.

if (on_match.has_matcher()) {
return [matcher_factory = std::move(create(on_match.matcher()))]() {
return OnMatch<DataType>{{}, matcher_factory()};
return [matcher_factory = std::move(create(on_match.matcher())),
keep_matching = on_match.keep_matching()]() {
return OnMatch<DataType>{{}, matcher_factory(), keep_matching};
};
} else if (on_match.has_action()) {
auto& factory = Config::Utility::getAndCheckFactory<ActionFactory<ActionFactoryContext>>(
Expand All @@ -337,7 +343,9 @@ class MatchTreeFactory : public OnMatchFactory<DataType> {

auto action_factory = factory.createActionFactoryCb(
*message, action_factory_context_, server_factory_context_.messageValidationVisitor());
return [action_factory] { return OnMatch<DataType>{action_factory, {}}; };
return [action_factory, keep_matching = on_match.keep_matching()] {
return OnMatch<DataType>{action_factory, {}, keep_matching};
};
}

return absl::nullopt;
Expand Down Expand Up @@ -367,6 +375,7 @@ class MatchTreeFactory : public OnMatchFactory<DataType> {
const std::string stats_prefix_;
ActionFactoryContext& action_factory_context_;
Server::Configuration::ServerFactoryContext& server_factory_context_;
MatchTreeValidationVisitor<DataType>& on_match_validation_visitor_;
MatchInputFactory<DataType> match_input_factory_;
};
} // namespace Matcher
Expand Down
2 changes: 2 additions & 0 deletions source/common/matcher/prefix_map_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ template <class DataType> class PrefixMapMatcher : public MapMatcher<DataType> {
absl::optional<OnMatch<DataType>> on_no_match, absl::Status& creation_status)
: MapMatcher<DataType>(std::move(data_input), std::move(on_no_match), creation_status) {}

// TODO(ravenblackx): Implement a Reentrant. Re-entry gives access to the
// shorter, matching prefixes for callers.
absl::optional<OnMatch<DataType>> doMatch(const std::string& data) override {
const auto result = children_.findLongestPrefix(data);
if (result) {
Expand Down
12 changes: 12 additions & 0 deletions source/common/matcher/validation_visitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ template <class DataType> class MatchTreeValidationVisitor {

const std::vector<absl::Status>& errors() const { return errors_; }

template <class OnMatchType> void validateOnMatch(const OnMatchType& on_match) {
if (!support_keep_matching_ && on_match.keep_matching()) {
errors_.emplace_back(
absl::InvalidArgumentError("keep_matching is not supported in this context"));
}
}

void setSupportKeepMatching(bool support_keep_matching) {
support_keep_matching_ = support_keep_matching;
}
Comment on lines +34 to +43
Copy link
Member

Choose a reason for hiding this comment

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

How this works for actual production scenarios?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intent is to reject configs that include keep_matching_ when used in a filter (or other context) that hasn't prepared to handle skipped matchers. Having a rejection during config validation, when users try the field where it's unexpected, was preferred to having matchers get silently skipped because there's no callback.


protected:
// Implementations would subclass this to specify the validation logic for data inputs,
// returning a helpful error message if validation fails.
Expand All @@ -39,6 +50,7 @@ template <class DataType> class MatchTreeValidationVisitor {

private:
std::vector<absl::Status> errors_;
bool support_keep_matching_ = false;
};
} // namespace Matcher
} // namespace Envoy
25 changes: 12 additions & 13 deletions source/extensions/common/matcher/trie_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ namespace Matcher {
using ::Envoy::Matcher::DataInputFactoryCb;
using ::Envoy::Matcher::DataInputGetResult;
using ::Envoy::Matcher::DataInputPtr;
using ::Envoy::Matcher::evaluateMatch;
using ::Envoy::Matcher::MatchState;
using ::Envoy::Matcher::MatchTree;
using ::Envoy::Matcher::OnMatch;
using ::Envoy::Matcher::OnMatchFactory;
using ::Envoy::Matcher::OnMatchFactoryCb;
using ::Envoy::Matcher::SkippedMatchCb;

template <class DataType> struct TrieNode {
size_t index_;
Expand Down Expand Up @@ -71,7 +71,8 @@ template <class DataType> class TrieMatcher : public MatchTree<DataType> {
}
}

typename MatchTree<DataType>::MatchResult match(const DataType& data) override {
typename MatchTree<DataType>::MatchResult
match(const DataType& data, SkippedMatchCb<DataType> skipped_match_cb = nullptr) override {
const auto input = data_input_->get(data);
if (input.data_availability_ != DataInputGetResult::DataAvailability::AllDataAvailable) {
return {MatchState::UnableToMatch, absl::nullopt};
Expand All @@ -93,22 +94,20 @@ template <class DataType> class TrieMatcher : public MatchTree<DataType> {
if (!first && node.exclusive_) {
continue;
}
if (node.on_match_->action_cb_) {
return {MatchState::MatchComplete, OnMatch<DataType>{node.on_match_->action_cb_, nullptr}};
}
// Resume any subtree matching to preserve backtracking progress.
auto matched = evaluateMatch(*node.on_match_->matcher_, data);
if (matched.match_state_ == MatchState::UnableToMatch) {
return {MatchState::UnableToMatch, absl::nullopt};
}
if (matched.match_state_ == MatchState::MatchComplete && matched.result_) {
return {MatchState::MatchComplete, OnMatch<DataType>{matched.result_, nullptr}};
// handleRecursionAndSkips should only return match-failure, no-match, or an action cb.
typename MatchTree<DataType>::MatchResult processed_match =
MatchTree<DataType>::handleRecursionAndSkips(*node.on_match_, data, skipped_match_cb);

if (processed_match.match_state_ != MatchState::MatchComplete ||
processed_match.on_match_.has_value()) {
return processed_match;
}
// No-match isn't definitive, so continue checking nodes.
if (first) {
first = false;
}
}
return {MatchState::MatchComplete, on_no_match_};
return MatchTree<DataType>::handleRecursionAndSkips(on_no_match_, data, skipped_match_cb);
}

private:
Expand Down
Loading
Loading