Skip to content

Commit e17a126

Browse files
authored
Make route prefix map matchers able to retry (#38986)
Commit Message: Make route prefix map matchers able to retry Additional Description: The concept of this change is that the current behavior is quite surprising; I believe what happens after this change is what a user would typically expect the prefix matcher behavior to be, but unfortunately changing it in-place would be a behavior change now that the existing behavior is established, so this needs a runtime guard. The difference with this version of the prefix match map is that the `on_no_match` field still runs if there was a subtree matcher that didn't match, whereas the old one requires each subtree to have its own `on_no_match`, resulting in a huge amount of duplicate config code for our use-case. Also, before falling back to the on_no_match, this variant tries any also-matching shorter prefixes, e.g. a prefix match map with retries like ``` "/banana": subtree_a, "/ban": subtree_b "/": subtree_c on_no_match ``` With a request path `/banana/is/a/fruit` would first try subtree_a, and if conditions in there don't return a match, would try subtree_b, then again to subtree_c, then finally the on_no_match action. Risk Level: A little; runtime guard allows reverting. Testing: Increased testing for existing config to ensure current [maybe unwanted] behavior, added tests for new behavior. Docs Changes: relnotes only. Release Notes: Yes. Platform Specific Features: n/a --------- Signed-off-by: Raven Black <[email protected]>
1 parent 4453ce1 commit e17a126

File tree

9 files changed

+273
-16
lines changed

9 files changed

+273
-16
lines changed

changelogs/current.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ behavior_changes:
77
As announced in November 2024 (see https://github.com/envoyproxy/envoy/issues/37621), the
88
grpc_credentials/aws_iam extension is being deleted. Any configuration referencing this extension
99
will fail to load.
10+
- area: prefix_match_map
11+
change: |
12+
:ref:`prefix_match_map <envoy_v3_api_field_config.common.matcher.v3.Matcher.MatcherTree.prefix_match_map>`
13+
now continues to search for a match with shorter prefix if a longer match
14+
does not find an action. This brings it in line with the behavior of ``matcher_list``.
15+
This change can temporarily be reverted by setting the runtime guard
16+
``envoy.reloadable_features.prefix_map_matcher_resume_after_subtree_miss`` to false.
17+
If the old behavior is desired more permanently, this can be achieved in config by setting
18+
an ``on_no_match`` action that responds with 404 for each subtree.
1019
1120
minor_behavior_changes:
1221
# *Changes that may cause incompatibilities for some users, but should not for most*

source/common/matcher/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ envoy_cc_library(
3030
deps = [
3131
":map_matcher_lib",
3232
"//source/common/common:trie_lookup_table_lib",
33+
"//source/common/runtime:runtime_features_lib",
3334
],
3435
)
3536

source/common/matcher/exact_map_matcher.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ template <class DataType> class ExactMapMatcher : public MapMatcher<DataType> {
3232
absl::optional<OnMatch<DataType>> on_no_match, absl::Status& creation_status)
3333
: MapMatcher<DataType>(std::move(data_input), std::move(on_no_match), creation_status) {}
3434

35-
absl::optional<OnMatch<DataType>> doMatch(const std::string& data) override {
36-
const auto itr = children_.find(data);
35+
typename MatchTree<DataType>::MatchResult doMatch(const DataType&, absl::string_view key,
36+
SkippedMatchCb<DataType>) override {
37+
const auto itr = children_.find(key);
3738
if (itr != children_.end()) {
38-
return itr->second;
39+
return {MatchState::MatchComplete, itr->second};
3940
}
4041

41-
return absl::nullopt;
42+
return {MatchState::MatchComplete, absl::nullopt};
4243
}
4344

4445
private:

source/common/matcher/map_matcher.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ class MapMatcher : public MatchTree<DataType>, Logger::Loggable<Logger::Id::matc
3232
return MatchTree<DataType>::handleRecursionAndSkips(on_no_match_, data, skipped_match_cb);
3333
}
3434

35-
const absl::optional<OnMatch<DataType>> result = doMatch(absl::get<std::string>(input.data_));
35+
const typename MatchTree<DataType>::MatchResult result =
36+
doMatch(data, absl::get<std::string>(input.data_), skipped_match_cb);
37+
if (result.match_state_ != MatchState::MatchComplete) {
38+
return result;
39+
}
3640
// No match.
37-
if (!result.has_value()) {
41+
if (!result.on_match_.has_value()) {
3842
// Match failed.
3943
if (input.data_availability_ ==
4044
DataInputGetResult::DataAvailability::MoreDataMightBeAvailable) {
@@ -46,7 +50,7 @@ class MapMatcher : public MatchTree<DataType>, Logger::Loggable<Logger::Id::matc
4650

4751
// Handle recursion and keep_matching.
4852
auto processed_result =
49-
MatchTree<DataType>::handleRecursionAndSkips(result, data, skipped_match_cb);
53+
MatchTree<DataType>::handleRecursionAndSkips(result.on_match_, data, skipped_match_cb);
5054
// Matched or failed nested matching.
5155
if (processed_result.match_state_ != MatchState::MatchComplete ||
5256
processed_result.on_match_.has_value()) {
@@ -74,7 +78,9 @@ class MapMatcher : public MatchTree<DataType>, Logger::Loggable<Logger::Id::matc
7478
// The inner match method. Attempts to match against the resulting data string. If the match
7579
// result was determined, the OnMatch will be returned. If a match result was determined to be no
7680
// match, {} will be returned.
77-
virtual absl::optional<OnMatch<DataType>> doMatch(const std::string& data) PURE;
81+
virtual typename MatchTree<DataType>::MatchResult
82+
doMatch(const DataType& data, absl::string_view key,
83+
SkippedMatchCb<DataType> skipped_match_cb) PURE;
7884
};
7985

8086
} // namespace Matcher

source/common/matcher/prefix_map_matcher.h

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "source/common/common/trie_lookup_table.h"
44
#include "source/common/matcher/map_matcher.h"
5+
#include "source/common/runtime/runtime_features.h"
56

67
namespace Envoy {
78
namespace Matcher {
@@ -30,15 +31,26 @@ template <class DataType> class PrefixMapMatcher : public MapMatcher<DataType> {
3031
absl::optional<OnMatch<DataType>> on_no_match, absl::Status& creation_status)
3132
: MapMatcher<DataType>(std::move(data_input), std::move(on_no_match), creation_status) {}
3233

33-
// TODO(ravenblackx): Implement a Reentrant. Re-entry gives access to the
34-
// shorter, matching prefixes for callers.
35-
absl::optional<OnMatch<DataType>> doMatch(const std::string& data) override {
36-
const auto result = children_.findLongestPrefix(data);
37-
if (result) {
38-
return *result;
34+
typename MatchTree<DataType>::MatchResult
35+
doMatch(const DataType& data, absl::string_view key,
36+
SkippedMatchCb<DataType> skipped_match_cb) override {
37+
const absl::InlinedVector<std::shared_ptr<OnMatch<DataType>>, 4> results =
38+
children_.findMatchingPrefixes(key);
39+
bool retry_shorter = Runtime::runtimeFeatureEnabled(
40+
"envoy.reloadable_features.prefix_map_matcher_resume_after_subtree_miss");
41+
for (auto it = results.rbegin(); it != results.rend(); ++it) {
42+
const std::shared_ptr<OnMatch<DataType>>& on_match = *it;
43+
typename MatchTree<DataType>::MatchResult result =
44+
MatchTree<DataType>::handleRecursionAndSkips(*on_match, data, skipped_match_cb);
45+
if (result.match_state_ != MatchState::MatchComplete || result.on_match_.has_value() ||
46+
!retry_shorter) {
47+
// If the match failed to complete, or if it matched, or
48+
// if we're doing the legacy "don't try additional matchers"
49+
// behavior, return whatever the first match's result was.
50+
return result;
51+
}
3952
}
40-
41-
return absl::nullopt;
53+
return {MatchState::MatchComplete, absl::nullopt};
4254
}
4355

4456
private:

source/common/runtime/runtime_features.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ RUNTIME_GUARD(envoy_reloadable_features_original_dst_rely_on_idle_timeout);
7070
RUNTIME_GUARD(envoy_reloadable_features_original_src_fix_port_exhaustion);
7171
RUNTIME_GUARD(envoy_reloadable_features_prefer_ipv6_dns_on_macos);
7272
RUNTIME_GUARD(envoy_reloadable_features_prefer_quic_client_udp_gro);
73+
RUNTIME_GUARD(envoy_reloadable_features_prefix_map_matcher_resume_after_subtree_miss);
7374
RUNTIME_GUARD(envoy_reloadable_features_proxy_104);
7475
RUNTIME_GUARD(envoy_reloadable_features_proxy_ssl_port);
7576
RUNTIME_GUARD(envoy_reloadable_features_proxy_status_mapping_more_core_response_flags);

test/common/matcher/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ envoy_cc_test(
8787
"//test/mocks/server:factory_context_mocks",
8888
"//test/mocks/stream_info:stream_info_mocks",
8989
"//test/test_common:registry_lib",
90+
"//test/test_common:test_runtime_lib",
9091
"@com_github_cncf_xds//xds/type/matcher/v3:pkg_cc_proto",
9192
"@envoy_api//envoy/config/common/matcher/v3:pkg_cc_proto",
9293
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",

test/common/matcher/matcher_test.cc

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "test/mocks/matcher/mocks.h"
1515
#include "test/mocks/server/factory_context.h"
1616
#include "test/test_common/registry.h"
17+
#include "test/test_common/test_runtime.h"
1718
#include "test/test_common/utility.h"
1819

1920
#include "gtest/gtest.h"
@@ -133,6 +134,213 @@ TEST_F(MatcherTest, TestPrefixMatcher) {
133134
EXPECT_THAT(result, HasResult(IsStringAction("expected!")));
134135
}
135136

137+
TEST_F(MatcherTest, TestPrefixMatcherWithRetryInnerMissPerformsOuterOnNoMatch) {
138+
const std::string yaml = R"EOF(
139+
on_no_match:
140+
action:
141+
name: test_action
142+
typed_config:
143+
"@type": type.googleapis.com/google.protobuf.StringValue
144+
value: expected!
145+
matcher_tree:
146+
input:
147+
name: outer_input
148+
typed_config:
149+
"@type": type.googleapis.com/google.protobuf.StringValue
150+
prefix_match_map:
151+
map:
152+
"":
153+
matcher:
154+
matcher_list:
155+
matchers:
156+
- predicate:
157+
single_predicate:
158+
input:
159+
name: inner_input
160+
typed_config:
161+
"@type": type.googleapis.com/google.protobuf.BoolValue
162+
value_match:
163+
exact: foo
164+
on_match:
165+
action:
166+
name: test_action
167+
typed_config:
168+
"@type": type.googleapis.com/google.protobuf.StringValue
169+
value: unexpected
170+
val:
171+
matcher:
172+
matcher_list:
173+
matchers:
174+
- predicate:
175+
single_predicate:
176+
input:
177+
name: inner_input
178+
typed_config:
179+
"@type": type.googleapis.com/google.protobuf.BoolValue
180+
value_match:
181+
exact: foo
182+
on_match:
183+
action:
184+
name: test_action
185+
typed_config:
186+
"@type": type.googleapis.com/google.protobuf.StringValue
187+
value: unexpected
188+
)EOF";
189+
190+
envoy::config::common::matcher::v3::Matcher matcher;
191+
MessageUtil::loadFromYaml(yaml, matcher, ProtobufMessage::getStrictValidationVisitor());
192+
193+
TestUtility::validate(matcher);
194+
195+
auto outer_factory = TestDataInputStringFactory("value");
196+
auto inner_factory = TestDataInputBoolFactory("bar");
197+
198+
EXPECT_CALL(validation_visitor_,
199+
performDataInputValidation(_, "type.googleapis.com/google.protobuf.StringValue"));
200+
EXPECT_CALL(validation_visitor_,
201+
performDataInputValidation(_, "type.googleapis.com/google.protobuf.BoolValue"))
202+
.Times(2);
203+
auto match_tree = factory_.create(matcher);
204+
205+
const auto result = match_tree()->match(TestData());
206+
EXPECT_THAT(result, HasStringAction("expected!"));
207+
}
208+
209+
TEST_F(MatcherTest, TestPrefixMatcherWithRetryInnerMissRetriesShorterPrefix) {
210+
const std::string yaml = R"EOF(
211+
matcher_tree:
212+
input:
213+
name: outer_input
214+
typed_config:
215+
"@type": type.googleapis.com/google.protobuf.StringValue
216+
prefix_match_map:
217+
map:
218+
val:
219+
matcher:
220+
matcher_list:
221+
matchers:
222+
- predicate:
223+
single_predicate:
224+
input:
225+
name: inner_input
226+
typed_config:
227+
"@type": type.googleapis.com/google.protobuf.BoolValue
228+
value_match:
229+
exact: bar
230+
on_match:
231+
action:
232+
name: test_action
233+
typed_config:
234+
"@type": type.googleapis.com/google.protobuf.StringValue
235+
value: expected!
236+
valu:
237+
matcher:
238+
matcher_list:
239+
matchers:
240+
- predicate:
241+
single_predicate:
242+
input:
243+
name: inner_input
244+
typed_config:
245+
"@type": type.googleapis.com/google.protobuf.BoolValue
246+
value_match:
247+
exact: foo
248+
on_match:
249+
action:
250+
name: test_action
251+
typed_config:
252+
"@type": type.googleapis.com/google.protobuf.StringValue
253+
value: not expected
254+
)EOF";
255+
256+
envoy::config::common::matcher::v3::Matcher matcher;
257+
MessageUtil::loadFromYaml(yaml, matcher, ProtobufMessage::getStrictValidationVisitor());
258+
259+
TestUtility::validate(matcher);
260+
261+
auto outer_factory = TestDataInputStringFactory("value");
262+
auto inner_factory = TestDataInputBoolFactory("bar");
263+
264+
EXPECT_CALL(validation_visitor_,
265+
performDataInputValidation(_, "type.googleapis.com/google.protobuf.StringValue"));
266+
EXPECT_CALL(validation_visitor_,
267+
performDataInputValidation(_, "type.googleapis.com/google.protobuf.BoolValue"))
268+
.Times(2);
269+
auto match_tree = factory_.create(matcher);
270+
271+
const auto result = match_tree()->match(TestData());
272+
EXPECT_THAT(result, HasStringAction("expected!"));
273+
}
274+
275+
TEST_F(MatcherTest, TestPrefixMatcherWithoutRetryInnerMissDoesNotRetryShorterPrefix) {
276+
TestScopedRuntime scoped_runtime;
277+
scoped_runtime.mergeValues(
278+
{{"envoy.reloadable_features.prefix_map_matcher_resume_after_subtree_miss", "false"}});
279+
const std::string yaml = R"EOF(
280+
matcher_tree:
281+
input:
282+
name: outer_input
283+
typed_config:
284+
"@type": type.googleapis.com/google.protobuf.StringValue
285+
prefix_match_map:
286+
map:
287+
val:
288+
matcher:
289+
matcher_list:
290+
matchers:
291+
- predicate:
292+
single_predicate:
293+
input:
294+
name: inner_input
295+
typed_config:
296+
"@type": type.googleapis.com/google.protobuf.BoolValue
297+
value_match:
298+
exact: bar
299+
on_match:
300+
action:
301+
name: test_action
302+
typed_config:
303+
"@type": type.googleapis.com/google.protobuf.StringValue
304+
value: expected!
305+
valu:
306+
matcher:
307+
matcher_list:
308+
matchers:
309+
- predicate:
310+
single_predicate:
311+
input:
312+
name: inner_input
313+
typed_config:
314+
"@type": type.googleapis.com/google.protobuf.BoolValue
315+
value_match:
316+
exact: foo
317+
on_match:
318+
action:
319+
name: test_action
320+
typed_config:
321+
"@type": type.googleapis.com/google.protobuf.StringValue
322+
value: not expected
323+
)EOF";
324+
325+
envoy::config::common::matcher::v3::Matcher matcher;
326+
MessageUtil::loadFromYaml(yaml, matcher, ProtobufMessage::getStrictValidationVisitor());
327+
328+
TestUtility::validate(matcher);
329+
330+
auto outer_factory = TestDataInputStringFactory("value");
331+
auto inner_factory = TestDataInputBoolFactory("bar");
332+
333+
EXPECT_CALL(validation_visitor_,
334+
performDataInputValidation(_, "type.googleapis.com/google.protobuf.StringValue"));
335+
EXPECT_CALL(validation_visitor_,
336+
performDataInputValidation(_, "type.googleapis.com/google.protobuf.BoolValue"))
337+
.Times(2);
338+
auto match_tree = factory_.create(matcher);
339+
340+
const auto result = match_tree()->match(TestData());
341+
EXPECT_THAT(result, HasNoMatch());
342+
}
343+
136344
TEST_F(MatcherTest, TestInvalidFloatPrefixMapMatcher) {
137345
const std::string yaml = R"EOF(
138346
matcher_tree:

test/common/matcher/prefix_map_matcher_test.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,23 @@ TEST(PrefixMapMatcherTest, MoreDataMightBeAvailableMatch) {
124124
verifyImmediateMatch(result, "match");
125125
}
126126

127+
TEST(PrefixMapMatcherTest, MoreDataMightBeAvailableNoMatchThenMatchDoesNotPerformSecondMatch) {
128+
std::unique_ptr<PrefixMapMatcher<TestData>> matcher = *PrefixMapMatcher<TestData>::create(
129+
std::make_unique<TestInput>(DataInputGetResult{
130+
DataInputGetResult::DataAvailability::MoreDataMightBeAvailable, "match"}),
131+
stringOnMatch<TestData>("no_match"));
132+
std::unique_ptr<PrefixMapMatcher<TestData>> child_matcher = *PrefixMapMatcher<TestData>::create(
133+
std::make_unique<TestInput>(DataInputGetResult{
134+
DataInputGetResult::DataAvailability::MoreDataMightBeAvailable, "match"}),
135+
absl::nullopt);
136+
137+
matcher->addChild("match", {nullptr, std::move(child_matcher)});
138+
matcher->addChild("mat", stringOnMatch<TestData>("second_match"));
139+
140+
TestData data;
141+
const auto result = matcher->match(data);
142+
verifyNotEnoughDataForMatch(result);
143+
}
144+
127145
} // namespace Matcher
128146
} // namespace Envoy

0 commit comments

Comments
 (0)