Make route prefix map matchers able to retry#38986
Conversation
Signed-off-by: Raven Black <[email protected]>
|
CC @envoyproxy/api-shepherds: Your approval is needed for changes made to |
|
/retest |
|
/retest |
|
/coverage |
|
Coverage for this Pull Request will be rendered here: The coverage results are (re-)rendered each time the CI |
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
|
Thanks for bringing this up! I agree that the current behavior seems strange. It seems like the behavior we really want here is that if a subtree does not find a match and does not have an I agree that for the prefix map matcher specifically, it makes sense that if a sub-tree is considered to not have matched, that we should look for other, less-specific prefix matches before defaulting to If we agree that this general principle is correct, then we have to figure out how to get to that behavior from where we are today. I see two main options:
I'd like input from @snowp, who initially wrote the matcher code. Also, I am wondering if this has any overlap with #38726 from @bsurber. |
|
This retry feature does look similar to what I had in mind when considering how to do re-entry for prefix map matchers. The largest difference is that a reentrant object would go back to the match caller, which is also capable of continuing "retries" (in this PR's parlance) from where the last match call ended. Implementation wise, I was considering doing the re-entry/retry by tracking each matching path along the way down the trie in a separate vector of pointers (optional parameter). Then the reentrant would just have to step backwards through that vector to find the next-longest match, instead of starting at the top of the trie all over again: O(n) vs O(n^2) execution at the cost of O(n) memory & enabling further re-entry at a specified index. |
I'm happy for it to be done that way - I was aiming for "minimal disruption" in my implementation, but I agree it would be nicer if it was made more generally "this is how it's supposed to work."
I also considered that. O(n) memory being the cost is a bit simplistic though - a vector is also another allocation, or potentially several, and O(n^2) is while technically accurate not really a sensible way to look at it when the real world case is going to be O(2n), maybe O(3n). A prefix config that's like Happy to defer to #38726 for implementation; I can contribute a vector-returning trie lookup if that would help. (Edit: #39011) |
|
Setting aside the question of implementation overlap with @bsurber's work, I think we still need to decide the overall API question here. The |
I think bsurber's implementation could do what we want. It would be quite a bit more verbose in the config than my model, but I'm assuming it would work to have a config like (pseudo-config): |
We could also avoid the vector output by adding optional parent references to the trie nodes. The reentrant would just need to track the current node & iterate up through the parents. It'd touch more code but could be preferable (requires touching the actual trie lib, not just the matchers). |
|
For the trie-based matcher, we actually do perform the traversal (see https://github.com/cncf/xds/blob/main/xds/type/matcher/v3/domain.proto#L39), so maybe we need to define a new kind of prefix matcher as a custom matcher? |
"The trie-based matcher" is a confusing thing to say, because prefix_map_matcher is also trie-based. Also confusing to link to that xds proto which doesn't seem to be used at all in envoy except in a fuzz test. I eventually figured out you meant Anyway, it certainly would be possible to implement this as just a custom matcher, if that's the preferred way. Bit weird since the API would be an exact duplicate of the existing API but it would have to either duplicate or wrap the existing message because custom extensions must use unique types. And mostly duplicate the code. And every time there's a custom extension point it makes the documentation painful to navigate. But if that's what it takes to get something useable I'm happy to go that way. |
Absolutely right about documentation. I don't think proto docs even cut it for the matching configuration, we need examples for specific use cases. I brought this up because we had a similar discussion when I introduced this custom matcher. The problem with the prefix matcher is that it's generic for strings, it is also used to match various authentication attributes (e.g. JWT attributes, TLS fields, etc) where it's not appropriate to have this "keep-matching" logic since |
Ah, thanks, I didn't realize it was used for other things, so yeah, that makes sense of it (though this implementation did control whether it does "keep matching" with a new flag in the config so it's not like it would be forcing unwanted behavior). Question: are you saying that looking back up in the case of a specific match being tagged with the "keep_matching" property (as in #38726) shouldn't cause prefix_map_matcher to "look up"? (If yes then I should implement a custom extension, but if no then I should just pay attention to bsurber's solution. Or if you're saying bsurber's solution is going to encounter a lot of pushback then that too means I should do it as a custom extension.) |
|
/wait-any for further consideration |
|
LGTM from an API perspective (even though this doesn't actually require API approval). |
| - area: prefix_match_map | ||
| change: | | ||
| :ref:`prefix_match_map <envoy_v3_api_field_config.common.matcher.v3.Matcher.MatcherTree.prefix_match_map>` | ||
| now continues to search for a match if the first subtree match |
There was a problem hiding this comment.
continue to search for a match with shorter prefix if the longest match does not find an action. While it's technically a map, it's a list in protobuf, so let's be explicit about ordering.
There was a problem hiding this comment.
Done (though I said "a longer match" rather than "the longest match" because it also does it for second-longest etc.)
| } | ||
| ASSERT(result->matcher_); | ||
| typename MatchTree<DataType>::MatchResult match = result->matcher_->match(data); | ||
| if (match.match_state_ != MatchState::MatchComplete || match.on_match_.has_value()) { |
There was a problem hiding this comment.
So this will not work with partial match, since we can no longer backtrack when an incomplete match gets returned here? Does this actually happen in Envoy when a MatchComplete is not returned for a prefix map?
There was a problem hiding this comment.
If a ListMatcher receives something other than a MatchComplete then yes, this same thing happens (I think this is correct, because you shouldn't resolve to a different result if you don't know yet that your first result didn't match.)
There was a problem hiding this comment.
Yes, you shouldn't, but if say the match later fails when the data arrives, there'd be no way to backtrack to a shorter prefix in the original tree. So partial match seems to be flawed to be "longest/first always wins". Is there some bug filed about this?
There was a problem hiding this comment.
I think one of us doesn't understand what's happening here, and I don't really know which, maybe both! Can you elaborate on "the match later fails when the data arrives"? My understanding is that when that happens either the whole lookup will run again from scratch when more data is available, or it will be aborted entirely. I can't see a way it would result in a wrong outcome. There's no continuation data with a MatchState::UnableToMatch response, so I'm pretty sure there's no path to "resolve to the wrong outcome".
But also I don't really understand how you would even provoke a MatchState::UnableToMatch response in production, nor what the behavior is when that result occurs, so it's very possible that I'm missing something. Also I don't know what you mean by "partial match", there only seems to be a match, a miss, or UnableToMatch, what is a partial match?
There was a problem hiding this comment.
It's worth double checking, since I'm not certain either! An example of this happening is matching on SNI with prefix match map, and then in the inner matcher, matching on a header (when that header is not yet received, that's possible in some HCM configuration). I see that evaluateMatch short-cuts the path when it's UnableToMatch so I think you're right, it will run the tree matcher from scratch when the data is received (which is worse performance-wise, but correct).
There was a problem hiding this comment.
Actually, I still don't get this logic -> it's not returning match_state_ upwards to the caller?
There was a problem hiding this comment.
You were right, this was a (pre-existing) issue. I think it may still be an issue that other matchers also don't bubble up UnableToMatch from submatchers (may be resolved after #38726). We probably should have a mandatory common test suite for matcher implementations rather than implementing these tests individually, to ensure that this sort of thing doesn't happen again. But that's for future cleanup, not for this PR.
|
/wait |
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
|
/retest |
kyessenov
left a comment
There was a problem hiding this comment.
LGTM, partial match correctness needs to be investigated later.
Signed-off-by: Raven Black <[email protected]>
Signed-off-by: Raven Black <[email protected]>
|
/retest |
Commit Message: Fix forward on a bad merge Additional Description: Broke a test by adding it and changing the test format at the same time. (#38986 + #39603) Risk Level: None Testing: Test-only Docs Changes: n/a Release Notes: n/a Platform Specific Features: n/a Signed-off-by: Raven Black <[email protected]>
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_matchfield still runs if there was a subtree matcher that didn't match, whereas the old one requires each subtree to have its ownon_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
With a request path
/banana/is/a/fruitwould 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